Repository: gruns/icecream Branch: master Commit: 1d3858e4346e Files: 24 Total size: 108.5 KB Directory structure: gitextract__hqhlpvx/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── changelog.txt ├── failures-to-investigate/ │ ├── freshsales.py │ ├── freshsales2.py │ └── freshsales3.py ├── icecream/ │ ├── __init__.py │ ├── __version__.py │ ├── builtins.py │ ├── coloring.py │ ├── icecream.py │ └── py.typed ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests/ │ ├── __init__.py │ ├── install_test_import.py │ ├── test_icecream.py │ └── test_install.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master pull_request: branches: - master jobs: build: runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: include: - python-version: '3.8' toxenv: py38 - python-version: '3.9' toxenv: py39 - python-version: '3.10' toxenv: py310 - python-version: '3.11' toxenv: py311 - python-version: '3.12' toxenv: py312 - python-version: '3.13' toxenv: py313 - python-version: 'pypy-3.10' toxenv: pypy3 - python-version: '3.9' toxenv: mypy steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install tox run: pip install tox - name: Tox run: tox env: TOXENV: ${{ matrix.toxenv }} ================================================ FILE: .gitignore ================================================ *~ .#* \#* .tox dist/ .eggs/ build/ *.pyc *.pyo *.egg *.egg-info .aider* playground ================================================ FILE: LICENSE.txt ================================================ Copyright 2018 Ansgar Grunseid Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE.txt README.md prune tests ================================================ FILE: README.md ================================================

IceCream

### IceCream — Never use print() to debug again Do you ever use `print()` or `log()` to debug your code? Of course you do. IceCream, or `ic` for short, makes print debugging a little sweeter. `ic()` is like `print()`, but better: 1. It prints both variables and expressions along with their values. 2. It's 60% faster to type. 3. Data structures are formatted and pretty printed. 4. Output is syntax highlighted. 5. It optionally includes program context: filename, line number, and parent function. IceCream is well tested, [permissively licensed](LICENSE.txt), and supports Python 3 and PyPy3. IceCream is maintained by [Jakeroid (Ivan Karabadzhak)](https://github.com/Jakeroid), with support from the confidential computing folks at [🌖 Lunal](https://lunal.dev/). ### Inspect Variables Have you ever printed variables or expressions to debug your program? If you've ever typed something like ```python print(foo('123')) ``` or the more thorough ```python print("foo('123')", foo('123')) ``` then `ic()` will put a smile on your face. With arguments, `ic()` inspects itself and prints both its own arguments and the values of those arguments. ```python from icecream import ic def foo(i): return i + 333 ic(foo(123)) ``` Prints ``` ic| foo(123): 456 ``` Similarly, ```python d = {'key': {1: 'one'}} ic(d['key'][1]) class klass(): attr = 'yep' ic(klass.attr) ``` Prints ``` ic| d['key'][1]: 'one' ic| klass.attr: 'yep' ``` Just give `ic()` a variable or expression and you're done. Easy. ### Inspect Execution Have you ever used `print()` to determine which parts of your program are executed, and in which order they're executed? For example, if you've ever added print statements to debug code like ```python def foo(): print(0) first() if expression: print(1) second() else: print(2) third() ``` then `ic()` helps here, too. Without arguments, `ic()` inspects itself and prints the calling filename, line number, and parent function. ```python from icecream import ic def foo(): ic() first() if expression: ic() second() else: ic() third() ``` Prints ``` ic| example.py:4 in foo() ic| example.py:11 in foo() ``` Just call `ic()` and you're done. Simple. ### Return Value `ic()` returns its argument(s), so `ic()` can easily be inserted into pre-existing code. ```pycon >>> a = 6 >>> def half(i): >>> return i / 2 >>> b = half(ic(a)) ic| a: 6 >>> ic(b) ic| b: 3 ``` ### Miscellaneous `ic.format(*args)` is like `ic()` but the output is returned as a string instead of written to stderr. ```pycon >>> from icecream import ic >>> s = 'sup' >>> out = ic.format(s) >>> print(out) ic| s: 'sup' ``` Additionally, `ic()`'s output can be entirely disabled, and later re-enabled, with `ic.disable()` and `ic.enable()` respectively. ```python from icecream import ic ic(1) ic.disable() ic(2) ic.enable() ic(3) ``` Prints ``` ic| 1: 1 ic| 3: 3 ``` `ic()` continues to return its arguments when disabled, of course; no existing code with `ic()` breaks. ### Import Tricks To make `ic()` available in every file without needing to be imported in every file, you can `install()` it. For example, in a root `A.py`: ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- from icecream import install install() from B import foo foo() ``` and then in `B.py`, which is imported by `A.py`, just call `ic()`: ```python # -*- coding: utf-8 -*- def foo(): x = 3 ic(x) ``` `install()` adds `ic()` to the [builtins](https://docs.python.org/3.8/library/builtins.html) module, which is shared amongst all files imported by the interpreter. Similarly, `ic()` can later be `uninstall()`ed, too. `ic()` can also be imported in a manner that fails gracefully if IceCream isn't installed, like in production environments (i.e. not development). To that end, this fallback import snippet may prove useful: ```python try: from icecream import ic except ImportError: # Graceful fallback if IceCream isn't installed. ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa ``` ### Configuration `ic.configureOutput(prefix, outputFunction, argToStringFunction, includeContext, contextAbsPath)` controls `ic()`'s output. `prefix`, if provided, adopts a custom output prefix. `prefix` can be a string, like ```pycon >>> from icecream import ic >>> ic.configureOutput(prefix='hello -> ') >>> ic('world') hello -> 'world' ``` or a function. ```pycon >>> import time >>> from icecream import ic >>> >>> def unixTimestamp(): >>> return '%i |> ' % int(time.time()) >>> >>> ic.configureOutput(prefix=unixTimestamp) >>> ic('world') 1519185860 |> 'world': 'world' ``` `prefix`'s default value is `ic| `. `outputFunction`, if provided, is called once for every `ic()` call with `ic()`'s output, as a string, instead of that string being written to stderr (the default). ```pycon >>> import logging >>> from icecream import ic >>> >>> def warn(s): >>> logging.warning("%s", s) >>> >>> ic.configureOutput(outputFunction=warn) >>> ic('eep') WARNING:root:ic| 'eep': 'eep' ``` `argToStringFunction`, if provided, is called with argument values to be serialized to displayable strings. The default is PrettyPrint's [pprint.pformat()](https://docs.python.org/3/library/pprint.html#pprint.pformat), but this can be changed to, for example, handle non-standard datatypes in a custom fashion. ```pycon >>> from icecream import ic >>> >>> def toString(obj): >>> if isinstance(obj, str): >>> return '[!string %r with length %i!]' % (obj, len(obj)) >>> return repr(obj) >>> >>> ic.configureOutput(argToStringFunction=toString) >>> ic(7, 'hello') ic| 7: 7, 'hello': [!string 'hello' with length 5!] ``` The default `argToStringFunction` is `icecream.argumentToString`, and has methods to `register` and `unregister` functions to be dispatched for specific classes using `functools.singledispatch`. It also has a `registry` property to view registered functions. ```pycon >>> from icecream import ic, argumentToString >>> import numpy as np >>> >>> # Register a function to summarize numpy array >>> @argumentToString.register(np.ndarray) >>> def _(obj): >>> return f"ndarray, shape={obj.shape}, dtype={obj.dtype}" >>> >>> x = np.zeros((1, 2)) >>> ic(x) ic| x: ndarray, shape=(1, 2), dtype=float64 >>> >>> # View registered functions >>> argumentToString.registry mappingproxy({object: , numpy.ndarray: }) >>> >>> # Unregister a function and fallback to the default behavior >>> argumentToString.unregister(np.ndarray) >>> ic(x) ic| x: array([[0., 0.]]) ``` `includeContext`, if provided and True, adds the `ic()` call's filename, line number, and parent function to `ic()`'s output. ```pycon >>> from icecream import ic >>> ic.configureOutput(includeContext=True) >>> >>> def foo(): >>> i = 3 >>> ic(i) >>> foo() ic| example.py:12 in foo()- i: 3 ``` `includeContext` is False by default. `contextAbsPath`, if provided and True, outputs absolute filepaths, like `/path/to/foo.py`, over just filenames, like `foo.py`, when `ic()` is called with `includeContext == True`. This is useful when debugging multiple files that share the same filename(s). Moreover, some editors, like VSCode, turn absolute filepaths into clickable links that open the file where `ic()` was called. ```pycon >>> from icecream import ic >>> ic.configureOutput(includeContext=True, contextAbsPath=True) >>> >>> i = 3 >>> >>> def foo(): >>> ic(i) >>> foo() ic| /absolute/path/to/example.py:12 in foo()- i: 3 >>> >>> ic.configureOutput(includeContext=True, contextAbsPath=False) >>> >>> def foo(): >>> ic(i) >>> foo() ic| example.py:18 in foo()- i: 3 ``` `contextAbsPath` is False by default. If you want to use icecream with multiple log levels, like with Python’s `logging` module, you can use `ic.format()` to integrate icecream’s debugging with your logger: ```python import logging from icecream import ic foo = 'bar' logging.debug(ic.format(foo)) ``` ❕ This is a bit clunky. Would you prefer built-in log level support in icecream? If so, please share your thoughts in [issue](https://github.com/gruns/icecream/issues/146). ### Installation Installing IceCream with pip is easy. ``` $ pip install icecream ``` ### Related Python libraries `ic()` uses [**`executing`**](https://github.com/alexmojaki/executing) by [**@alexmojaki**](https://github.com/alexmojaki) to reliably locate `ic()` calls in Python source. It's magic. ### IceCream in Other Languages Delicious IceCream should be enjoyed in every language. - Dart: [icecream](https://github.com/HallerPatrick/icecream) - Rust: [icecream-rs](https://github.com/ericchang00/icecream-rs) - Node.js: [node-icecream](https://github.com/jmerle/node-icecream) - C++: [IceCream-Cpp](https://github.com/renatoGarcia/icecream-cpp) - C99: [icecream-c](https://github.com/chunqian/icecream-c) - PHP: [icecream-php](https://github.com/ntzm/icecream-php) - Go: [icecream-go](https://github.com/WAY29/icecream-go) - Ruby: [Ricecream](https://github.com/nodai2hITC/ricecream) - Java: [icecream-java](https://github.com/Akshay-Thakare/icecream-java) - R: [icecream](https://github.com/lewinfox/icecream) - Lua: [icecream-lua](https://github.com/wlingze/icecream-lua) - Clojure(Script): [icecream-cljc](https://github.com/Eigenbahn/icecream-cljc) - Bash: [IceCream-Bash](https://github.com/jtplaarj/IceCream-Bash) - SystemVerilog: [icecream_sv](https://github.com/xver/icecream_sv) - GameMaker Language: [GMIceCream](https://github.com/dicksonlaw583/GMIceCream) If you'd like a similar `ic()` function in your favorite language, please open a pull request! IceCream's goal is to sweeten print debugging with a handy-dandy `ic()` function in every language. ================================================ FILE: changelog.txt ================================================ ================================================================================ v2.1.10 ================================================================================ Improved: This change excludes the test folder from wheels. Big thanks to the community! This release was made possible by the people who contributed to the library. ================================================================================ v2.1.9 ================================================================================ Removed: Support for Python 3.8. Fixed: Issues #229 and #60, which means improved lists output. Big thanks to the community! This release was made possible by the people who contributed to the library. ================================================================================ v2.1.8 ================================================================================ Added: You can pass a pre-configured ic instance to builtins. Added: You can configure IceCream to output to either stdout or stderr. Big thanks to the community! This release was made possible by the people who contributed to the library. ================================================================================ v2.1.7 ================================================================================ Added: Configurable line wrap length. Improved: The package no longer includes tests in the production installation. ================================================================================ v2.1.6 ================================================================================ Fixed: Pretty-printing of SymPy (and similar) objects. Previously, calling ic() on structures containing SymPy objects could raise a TypeError because pprint.pformat(sort_dicts=True) attempted to sort unorderable keys. IceCream now keeps sort_dicts=True on the fast path and falls back to sort_dicts=False when pprint raises, ensuring robust output without crashes. ================================================================================ v2.1.5 ================================================================================ Changed: Improved printing for variables of type `str`. Fixed issues that affected the output of multiline strings and strings containing special characters such as escaped newlines and tabs. Strings are now printed exactly as they are, faithfully representing their actual value. ================================================================================ v2.1.4 ================================================================================ Changed: Drop support for all Python versions prior to Python 3.8, which are now long past EOL. Notably: Python 2 is no longer supported. Changed: Update the 'executing' dependency to >= v2.1.0 to improve source code analysis and support Python 3.13. ================================================================================ v2.1.3 ================================================================================ Added: The contextAbsPath= parameter to ic.configureOutput() which, when True, outputs absolute paths, like /path/to/foo.py, instead of just filenames, like foo.py. See https://github.com/gruns/icecream/pull/122. Huge thank you to @HelinXu! Changed: Raise TypeError if no arguments are provided to ic.configureOutput(). ================================================================================ v2.1.2 ================================================================================ Added: Ability to register and unregister singledispatch argumentToString functions. See https://github.com/gruns/icecream/pull/115. Huge thank you to @atusy! ================================================================================ v2.1.1 ================================================================================ Added: Support for Python 3.9. Changed: Use timestamps in the local timezone instead of less helpful UTC timestamps. ================================================================================ v2.1.0 ================================================================================ Added: install() and uninstall() functions that add or remove ic() from the builtins module. Changed: Switch to ast.literal_eval() to determine if an argument and value are the same, and thus only the value should be output. Huge thank you to Ed Cardinal and Alex Hall. ================================================================================ v2.0.0 ================================================================================ Added: Support for Python 3.8. Removed: Support for Python 3.4. Changed: Switched core AST parsing engine to Alex Hall's executing (https://github.com/alexmojaki/executing). Huge thank you to Alex Hall. Changed: Whitespace in arguments is no longer collapsed. Indentation in multiline arguments is now preserved. ================================================================================ v1.5.0 ================================================================================ Fixed: Support multiline container arguments. e.g. ic([a, b]) Fixed: Include LICENSE.txt in source distributions. Changed: Collapse argument whitespace, e.g. ic([ a, b ]) -> ic| [a, b]. ================================================================================ v1.4.0 ================================================================================ Added: Colorize output with pygments. Added: Test Python style with pycodestyle. Fixed: Parse and print tuple arguments correctly, e.g. ic((a, b)). Fixed: Fail gracefully when the underlying source code changes during execution. Changed: Print values (e.g. 1, 'foo', etc) by themselves, nonredundantly. For example, ic(3) now prints 'ic| 3' instead of 'ic| 3: 3'. ================================================================================ v1.3.1 ================================================================================ Removed: Support for Python 3.3, which reached EOL on 2017-09-29. Fixed: ic() invocations that fail to find or access source code (e.g. eval(), exec(), python -i, etc) now print an error message instead of throwing an IOError (Python 2) or OSError (Python 3). ================================================================================ v1.3 ================================================================================ First release. This changelog wasn't maintained prior to v1.3. ================================================ FILE: failures-to-investigate/freshsales.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright _!_ # # License _!_ from os.path import abspath, dirname, join as pjoin import pprint import sys import time import requests from icecream import ic _corePath = abspath(pjoin(dirname(__file__), '../')) if _corePath not in sys.path: sys.path.append(_corePath) from common.utils import lget DEFAULT_FIRST_NAME = 'there' DEFAULT_LAST_NAME = '-' FRESH_SALES_API_KEY = 'P3bYheaquAHH1_hNxhMUDQ' FS_API_URL = 'https://arcindustriesinc.freshsales.io/api' FS_AUTH_HEADERS = {'Authorization': 'Token token=P3bYheaquAHH1_hNxhMUDQ'} # # FreshSales' Contact field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/contacts/fields" # # # FreshSales' Lead field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/leads/fields" # # FreshSales' Company field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/sales_accounts/fields" # def splitName(name): # Intelligently split into first and last name, if provided. # '' -> firstName: '', lastName: '-' # 'Susan' -> firstName: 'Susan', lastName: '-' # 'Greg Borp' -> firstName: 'Greg', lastName: 'Borp' # 'Freddy van der Field' -> firstName: 'Freddy', lastName: 'van der Field' toks = name.split(None, 1) firstName = lget(toks, 0, DEFAULT_FIRST_NAME) lastName = lget(toks, 1, DEFAULT_LAST_NAME) return firstName, lastName def lookupFullContact(contact): contactId = contact['id'] resp = requests.get( f'{FS_API_URL}/contacts/{contactId}?include=sales_accounts', headers=FS_AUTH_HEADERS) contact = (resp.json() or {}).get('contact') return contact def findFirstContactWithEmail(emailAddr): return _findFirstEntityOf('contact', 'email', emailAddr) def findFirstCompanyWithWebsite(websiteUrl): return _findFirstEntityOf('sales_account', 'website', websiteUrl) def _findFirstEntityOf(entityType, query, queryValue): url = f'{FS_API_URL}/lookup?f={query}&entities={entityType}' from furl import furl ic(url, furl(f'{FS_API_URL}/lookup?f={query}&entities={entityType}').set( {'q': queryValue}).url) resp = requests.get( f'{FS_API_URL}/lookup?f={query}&entities={entityType}', params={'q': queryValue}, headers=FS_AUTH_HEADERS) entities = ( resp.json() or {}).get(f'{entityType}s', {}).get(f'{entityType}s', []) entity = lget(entities, 0) return entity def createNote(entityType, entityId, message): data = { 'note': { 'description': message, 'targetable_id': entityId, 'targetable_type': entityType, } } resp = requests.post( f'{FS_API_URL}/notes', json=data, headers=FS_AUTH_HEADERS) if resp.status_code != 201: err = f'Failed to create {entityType} note for id {entityId}.' raise RuntimeError(err) def createLead(data): return _createEntity('lead', data) def createContact(data): ANSGAR_GRUNSEID = 9000013180 data.setdefault('owner_id', ANSGAR_GRUNSEID) return _createEntity('contact', data) def createCompany(data): return _createEntity('sales_account', data) def _createEntity(entityType, data): wrapped = {entityType: data} url = f'{FS_API_URL}/{entityType}s' resp = requests.post(url, json=wrapped, headers=FS_AUTH_HEADERS) if resp.status_code not in [200, 201]: raise RuntimeError(f'Failed to create new {entityType}.') entity = (resp.json() or {}).get(entityType) return entity def updateLead(leadId, data): return _updateEntity('lead', leadId, data) def updateContact(contactId, data): return _updateEntity('contact', contactId, data) def updateCompany(companyId, data): return _updateEntity('sales_account', companyId, data) def _updateEntity(entityType, entityId, data): wrapped = {entityType: data} url = f'{FS_API_URL}/{entityType.lower()}s/{entityId}' resp = requests.put(url, json=wrapped, headers=FS_AUTH_HEADERS) if resp.status_code != 200: err = f'Failed to update {entityType.title()} with id {entityId}.' raise RuntimeError(err) entity = (resp.json() or {}).get(entityType) return entity def lookupContactsInView(viewId): return _lookupEntitiesInView('contact', viewId) def _lookupEntitiesInView(entityType, viewId): entities = [] url = f'{FS_API_URL}/{entityType.lower()}s/view/{viewId}' def pageUrl(pageNo): return url + f'?page={pageNo}' resp = requests.get(url, headers=FS_AUTH_HEADERS) js = resp.json() entities += js.get(f'{entityType}s') totalPages = js.get('meta', {}).get('total_pages') for pageNo in range(2, totalPages + 1): resp = requests.get(pageUrl(pageNo), headers=FS_AUTH_HEADERS) entities += (resp.json() or {}).get(f'{entityType}s') return entities def unsubscribeContact(contact, reasons): UNSUBSCRIBED = 9000159966 updateContact(contact['id'], { 'do_not_disturb': True, 'contact_status_id': UNSUBSCRIBED, }) dateStr = time.ctime() reasonsStr = pprint.pformat(reasons) note = ( f'This Contact unsubscribed on arc.io/unsubscribe at [{dateStr}] ' 'because:\n' '\n' f'{reasonsStr}\n' '\n') createNote('Contact', contact['id'], note) def optContactIn(contact): OPTED_IN = 9000159976 updateContact(contact['id'], { 'contact_status_id': OPTED_IN, }) def createAndOrAssociateCompanyWithContact(websiteUrl, contact): if 'sales_accounts' not in contact: contact = lookupFullContact(contact) companyToAdd = None companies = contact.get('sales_accounts', []) company = findFirstCompanyWithWebsite(websiteUrl) if company: companyId = company['id'] alreadyRelated = any(companyId == c['id'] for c in companies) if not alreadyRelated: companyToAdd = company else: companyToAdd = createCompany({ 'name': websiteUrl, 'website': websiteUrl, }) if companyToAdd: companyData = { 'id': companyToAdd['id'], # There can only be one primary Company associated with a # Contact. See https://www.freshsales.io/api/#create_contact. 'is_primary': False if companies else True, } companies.append(companyData) updateContact(contact['id'], { 'sales_accounts': companies }) return company or companyToAdd def upgradeContactWhoSubmittedSplashPage(contact, websiteUrl): createAndOrAssociateCompanyWithContact(websiteUrl, contact) SUBMITTED_ARC_IO_SIGN_UP_FORM = 9000159955 updateContact(contact['id'], { 'contact_status_id': SUBMITTED_ARC_IO_SIGN_UP_FORM, }) dateStr = time.ctime() emailAddr = contact['email'] note = ( f'This Contact submitted the sign up form on arc.io at [{dateStr}] ' f'with email address [{emailAddr}] and website [{websiteUrl}].') createNote('Contact', contact['id'], note) def noteContactSubmittedPepSplashPage(contact, websiteUrl): createAndOrAssociateCompanyWithContact(websiteUrl, contact) PEP = 9000004543 updateContact(contact['id'], { 'custom_field': { 'cf_product': 'Pep', }, }) dateStr = time.ctime() emailAddr = contact['email'] note = ( f"This Contact submitted Pep's sign up form on pep.dev at [{dateStr}] " f'with email address [{emailAddr}] and website [{websiteUrl}].') createNote('Contact', contact['id'], note) def createCrawledIndieHackersContact(name, emailAddr, websiteUrl, noteData): INDIE_HACKERS = 9000321821 _createCrawledContact(name, emailAddr, websiteUrl, INDIE_HACKERS, noteData) def _createCrawledContact(name, emailAddr, websiteUrl, leadSourceId, noteData): firstName, lastName = splitName(name) SUSPECT = 9000073090 contact = createContact({ 'email': emailAddr, 'first_name': firstName, 'last_name': lastName, 'contact_status_id': SUSPECT, 'lead_source_id': leadSourceId, }) createAndOrAssociateCompanyWithContact(websiteUrl, contact) dateStr = time.ctime() reasonsStr = pprint.pformat(noteData) note = ( f'This Contact was crawled and created on [{dateStr}]. ' 'Other data:' '\n' f'{reasonsStr}\n' '\n') createNote('Contact', contact['id'], note) def createSplashPageLead(name, emailAddr, websiteUrl): firstName, lastName = splitName(name) INTERESTED = 9000057526 ARC_IO_SIGN_UP_FORM = 9000315608 lead = createLead({ 'first_name': firstName, 'last_name': lastName, 'email': emailAddr, 'company': { 'website': websiteUrl, }, 'lead_stage_id': INTERESTED, 'lead_source_id': ARC_IO_SIGN_UP_FORM, }) dateStr = time.ctime() note = ( f'This Lead was created on [{dateStr}] because they submitted ' f'the sign up form on arc.io with email address [{emailAddr}] ' f'and website [{websiteUrl}].') createNote('Lead', lead['id'], note) def createPepSplashPageLead(emailAddr, websiteUrl): PEP = 9000004543 INTERESTED = 9000057526 PEP_SIGN_UP_FORM = 9000321929 lead = createLead({ 'first_name': DEFAULT_FIRST_NAME, 'last_name': DEFAULT_LAST_NAME, 'email': emailAddr, 'company': { 'website': websiteUrl, }, 'deal': { 'deal_product_id': PEP, }, 'lead_stage_id': INTERESTED, 'lead_source_id': PEP_SIGN_UP_FORM, }) dateStr = time.ctime() note = ( f'This Lead was created on [{dateStr}] because they submitted ' f'the sign up form on pep.dev with email address [{emailAddr}] ' f'and website [{websiteUrl}].') createNote('Lead', lead['id'], note) def noteACustomersFirstWidgetReport(emailAddr, seenOnUrl): raise NotImplementedError # TODO(grun): Finish me. contact = findFirstContactWithEmail(emailAddr) if contact: note = ( f'The widget for Arc account with email {emailAddr} was just seen ' f'live for the first seen for the first time live on {seenOnUrl}.') createNote('Contact', contact['id'], note) else: # TODO(grun): Log this scenario, which means someone added Arc's widget # to someone ic() # TODO(grun): Refactor and/or rename the below handleWordPressPlugin*() # functions, like how the above handle*() functions were refactored. def handleWordPressPluginInstall(emailAddr, websiteUrl): WORDPRESS = 9000321857 ALPHA_CODE = 9000124404 contact = findFirstContactWithEmail(emailAddr) if contact: updateContact(contact['id'], { 'lead_source_id': WORDPRESS, 'contact_status_id': ALPHA_CODE, }) else: contact = createContact({ 'email': emailAddr, 'first_name': 'there', 'last_name': websiteUrl, 'lead_source_id': WORDPRESS, 'contact_status_id': ALPHA_CODE, }) CUSTOMER = 9000095000 company = createAndOrAssociateCompanyWithContact(websiteUrl, contact) updateCompany(company['id'], { 'business_type_id': CUSTOMER, 'custom_field': { 'cf_source': 'Wordpress', }, }) dateStr = time.ctime() note = ( f"This Contact installed Arc's WordPress plugin at [{dateStr}] on " "website [{websiteUrl}].") createNote('Contact', contact['id'], note) def handleWordPressPluginCreatedArcAccount(emailAddr): contact = findFirstContactWithEmail(emailAddr) if not contact: return CUSTOMER = 9000066454 updateContact(contact['id'], { 'contact_status_id': CUSTOMER }) dateStr = time.ctime() note = ( f'This WordPress Contact created their Arc account at [{dateStr}].') createNote('Contact', contact['id'], note) def handleWordPressPluginUninstall(emailAddr): contact = findFirstContactWithEmail(emailAddr) if not contact: return FORMER_CUSTOMER = 9000124405 updateContact(contact['id'], { 'contact_status_id': FORMER_CUSTOMER }) dateStr = time.ctime() note = ( f'This Contact uninstalled their WordPress plugin at [{dateStr}].') createNote('Contact', contact['id'], note) if __name__ == '__main__': # For development only. ic(findFirstCompanyWithWebsite('http://blockchainexamples.com')) ================================================ FILE: failures-to-investigate/freshsales2.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright _!_ # # License _!_ from os.path import abspath, dirname, join as pjoin import pprint import sys import time from urllib.parse import urlparse import requests from icecream import ic _corePath = abspath(pjoin(dirname(__file__), '../')) if _corePath not in sys.path: sys.path.append(_corePath) from common.utils import lget, stripStringStart DEFAULT_FIRST_NAME = 'there' DEFAULT_LAST_NAME = '-' FRESH_SALES_API_KEY = 'P3bYheaquAHH1_hNxhMUDQ' FS_API_URL = 'https://arcindustriesinc.freshsales.io/api' FS_AUTH_HEADERS = {'Authorization': 'Token token=P3bYheaquAHH1_hNxhMUDQ'} # # FreshSales' Contact field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/contacts/fields" # # # FreshSales' Lead field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/leads/fields" # # FreshSales' Company field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/sales_accounts/fields" # def splitName(name): # Intelligently split into first and last name, if provided. # '' -> firstName: '', lastName: '-' # 'Susan' -> firstName: 'Susan', lastName: '-' # 'Greg Borp' -> firstName: 'Greg', lastName: 'Borp' # 'Freddy van der Field' -> firstName: 'Freddy', lastName: 'van der Field' toks = name.split(None, 1) firstName = lget(toks, 0, DEFAULT_FIRST_NAME) lastName = lget(toks, 1, DEFAULT_LAST_NAME) return firstName, lastName def lookupFullContact(contact): contactId = contact['id'] resp = requests.get( f'{FS_API_URL}/contacts/{contactId}?include=sales_accounts', headers=FS_AUTH_HEADERS) contact = (resp.json() or {}).get('contact') return contact def findFirstContactWithEmail(emailAddr): contacts = _findEntitiesWith('contact', 'email', emailAddr) return lget(contacts, 0) def findFirstCompanyWithWebsite(websiteUrl): # FreshSales' API returns unrelated companies. For example, searching for # companies with website 'http://blockchainexamples.com', ie # # ?f=website&entities=sales_account&q=http%3A%2F%2Fblockchainexamples.com # # returns https://arcindustriesinc.freshsales.io/accounts/9001963743 with # name 'http://culturetv.club' and website # 'http://104.225.221.170:8082'. Why? Who knows. # # As a workaround, filter all returned companies to verify that the domains # match. hostNoWww = lambda url: stripStringStart(urlparse(url).hostname, 'www.') allCompanies = _findEntitiesWith('sales_account', 'website', websiteUrl) ic(allCompanies) companies = [ c for c in _findEntitiesWith('sales_account', 'website', websiteUrl) if ic(hostNoWww(c.get('website'))) == ic(hostNoWww(websiteUrl))] firstCompany = lget(companies, 0) return firstCompany def _findEntitiesWith(entityType, query, queryValue): resp = requests.get( f'{FS_API_URL}/lookup?f={query}&entities={entityType}', params={'q': queryValue}, headers=FS_AUTH_HEADERS) entities = ( resp.json() or {}).get(f'{entityType}s', {}).get(f'{entityType}s', []) return entities def createNote(entityType, entityId, message): data = { 'note': { 'description': message, 'targetable_id': entityId, 'targetable_type': entityType, } } resp = requests.post( f'{FS_API_URL}/notes', json=data, headers=FS_AUTH_HEADERS) if resp.status_code != 201: err = f'Failed to create {entityType} note for id {entityId}.' raise RuntimeError(err) def createLead(data): return _createEntity('lead', data) def createContact(data): ANSGAR_GRUNSEID = 9000013180 data.setdefault('owner_id', ANSGAR_GRUNSEID) return _createEntity('contact', data) def createCompany(data): return _createEntity('sales_account', data) def _createEntity(entityType, data): wrapped = {entityType: data} url = f'{FS_API_URL}/{entityType}s' resp = requests.post(url, json=wrapped, headers=FS_AUTH_HEADERS) if resp.status_code not in [200, 201]: raise RuntimeError(f'Failed to create new {entityType}.') entity = (resp.json() or {}).get(entityType) return entity def updateLead(leadId, data): return _updateEntity('lead', leadId, data) def updateContact(contactId, data): return _updateEntity('contact', contactId, data) def updateCompany(companyId, data): return _updateEntity('sales_account', companyId, data) def _updateEntity(entityType, entityId, data): wrapped = {entityType: data} url = f'{FS_API_URL}/{entityType.lower()}s/{entityId}' resp = requests.put(url, json=wrapped, headers=FS_AUTH_HEADERS) if resp.status_code != 200: err = f'Failed to update {entityType.title()} with id {entityId}.' raise RuntimeError(err) entity = (resp.json() or {}).get(entityType) return entity def lookupContactsInView(viewId): return _lookupEntitiesInView('contact', viewId) def _lookupEntitiesInView(entityType, viewId): entities = [] url = f'{FS_API_URL}/{entityType.lower()}s/view/{viewId}' def pageUrl(pageNo): return url + f'?page={pageNo}' resp = requests.get(url, headers=FS_AUTH_HEADERS) js = resp.json() entities += js.get(f'{entityType}s') totalPages = js.get('meta', {}).get('total_pages') for pageNo in range(2, totalPages + 1): resp = requests.get(pageUrl(pageNo), headers=FS_AUTH_HEADERS) entities += (resp.json() or {}).get(f'{entityType}s') return entities def unsubscribeContact(contact, reasons): UNSUBSCRIBED = 9000159966 updateContact(contact['id'], { 'do_not_disturb': True, 'contact_status_id': UNSUBSCRIBED, }) dateStr = time.ctime() reasonsStr = pprint.pformat(reasons) note = ( f'This Contact unsubscribed on arc.io/unsubscribe at [{dateStr}] ' 'because:\n' '\n' f'{reasonsStr}\n' '\n') createNote('Contact', contact['id'], note) def optContactIn(contact): OPTED_IN = 9000159976 updateContact(contact['id'], { 'contact_status_id': OPTED_IN, }) def createAndOrAssociateCompanyWithContact(websiteUrl, contact): if 'sales_accounts' not in contact: contact = lookupFullContact(contact) companyToAdd = None companies = contact.get('sales_accounts', []) company = findFirstCompanyWithWebsite(websiteUrl) if company: companyId = company['id'] alreadyRelated = any(companyId == c['id'] for c in companies) if not alreadyRelated: companyToAdd = company else: companyToAdd = createCompany({ 'name': websiteUrl, 'website': websiteUrl, }) if companyToAdd: companyData = { 'id': companyToAdd['id'], # There can only be one primary Company associated with a # Contact. See https://www.freshsales.io/api/#create_contact. 'is_primary': False if companies else True, } companies.append(companyData) updateContact(contact['id'], { 'sales_accounts': companies }) return company or companyToAdd def upgradeContactWhoSubmittedSplashPage(contact, websiteUrl): createAndOrAssociateCompanyWithContact(websiteUrl, contact) SUBMITTED_ARC_IO_SIGN_UP_FORM = 9000159955 updateContact(contact['id'], { 'contact_status_id': SUBMITTED_ARC_IO_SIGN_UP_FORM, }) dateStr = time.ctime() emailAddr = contact['email'] note = ( f'This Contact submitted the sign up form on arc.io at [{dateStr}] ' f'with email address [{emailAddr}] and website [{websiteUrl}].') createNote('Contact', contact['id'], note) def noteContactSubmittedPepSplashPage(contact, websiteUrl): createAndOrAssociateCompanyWithContact(websiteUrl, contact) PEP = 9000004543 updateContact(contact['id'], { 'custom_field': { 'cf_product': 'Pep', }, }) dateStr = time.ctime() emailAddr = contact['email'] note = ( f"This Contact submitted Pep's sign up form on pep.dev at [{dateStr}] " f'with email address [{emailAddr}] and website [{websiteUrl}].') createNote('Contact', contact['id'], note) def createCrawledIndieHackersContact(name, emailAddr, websiteUrl, noteData): INDIE_HACKERS = 9000321821 _createCrawledContact(name, emailAddr, websiteUrl, INDIE_HACKERS, noteData) def _createCrawledContact(name, emailAddr, websiteUrl, leadSourceId, noteData): firstName, lastName = splitName(name) SUSPECT = 9000073090 contact = createContact({ 'email': emailAddr, 'first_name': firstName, 'last_name': lastName, 'contact_status_id': SUSPECT, 'lead_source_id': leadSourceId, }) createAndOrAssociateCompanyWithContact(websiteUrl, contact) dateStr = time.ctime() reasonsStr = pprint.pformat(noteData) note = ( f'This Contact was crawled and created on [{dateStr}]. ' 'Other data:' '\n' f'{reasonsStr}\n' '\n') createNote('Contact', contact['id'], note) def createSplashPageLead(name, emailAddr, websiteUrl): firstName, lastName = splitName(name) INTERESTED = 9000057526 ARC_IO_SIGN_UP_FORM = 9000315608 lead = createLead({ 'first_name': firstName, 'last_name': lastName, 'email': emailAddr, 'company': { 'website': websiteUrl, }, 'lead_stage_id': INTERESTED, 'lead_source_id': ARC_IO_SIGN_UP_FORM, }) dateStr = time.ctime() note = ( f'This Lead was created on [{dateStr}] because they submitted ' f'the sign up form on arc.io with email address [{emailAddr}] ' f'and website [{websiteUrl}].') createNote('Lead', lead['id'], note) def createPepSplashPageLead(emailAddr, websiteUrl): PEP = 9000004543 INTERESTED = 9000057526 PEP_SIGN_UP_FORM = 9000321929 lead = createLead({ 'first_name': DEFAULT_FIRST_NAME, 'last_name': DEFAULT_LAST_NAME, 'email': emailAddr, 'company': { 'website': websiteUrl, }, 'deal': { 'deal_product_id': PEP, }, 'lead_stage_id': INTERESTED, 'lead_source_id': PEP_SIGN_UP_FORM, }) dateStr = time.ctime() note = ( f'This Lead was created on [{dateStr}] because they submitted ' f'the sign up form on pep.dev with email address [{emailAddr}] ' f'and website [{websiteUrl}].') createNote('Lead', lead['id'], note) def noteACustomersFirstWidgetReport(emailAddr, seenOnUrl): raise NotImplementedError # TODO(grun): Finish me. contact = findFirstContactWithEmail(emailAddr) if contact: note = ( f'The widget for Arc account with email {emailAddr} was just seen ' f'live for the first seen for the first time live on {seenOnUrl}.') createNote('Contact', contact['id'], note) else: # TODO(grun): Log this scenario, which means someone added Arc's widget # to someone ic() # TODO(grun): Refactor and/or rename the below handleWordPressPlugin*() # functions, like how the above handle*() functions were refactored. def handleWordPressPluginInstall(emailAddr, websiteUrl): WORDPRESS = 9000321857 ALPHA_CODE = 9000124404 contact = findFirstContactWithEmail(emailAddr) if contact: updateContact(contact['id'], { 'lead_source_id': WORDPRESS, 'contact_status_id': ALPHA_CODE, }) else: contact = createContact({ 'email': emailAddr, 'first_name': 'there', 'last_name': websiteUrl, 'lead_source_id': WORDPRESS, 'contact_status_id': ALPHA_CODE, }) CUSTOMER = 9000095000 company = createAndOrAssociateCompanyWithContact(websiteUrl, contact) updateCompany(company['id'], { 'business_type_id': CUSTOMER, 'custom_field': { 'cf_source': 'Wordpress', }, }) dateStr = time.ctime() note = ( f"This Contact installed Arc's WordPress plugin at [{dateStr}] on " "website [{websiteUrl}].") createNote('Contact', contact['id'], note) def handleWordPressPluginCreatedArcAccount(emailAddr): contact = findFirstContactWithEmail(emailAddr) if not contact: return CUSTOMER = 9000066454 updateContact(contact['id'], { 'contact_status_id': CUSTOMER }) dateStr = time.ctime() note = ( f'This WordPress Contact created their Arc account at [{dateStr}].') createNote('Contact', contact['id'], note) def handleWordPressPluginUninstall(emailAddr): contact = findFirstContactWithEmail(emailAddr) if not contact: return FORMER_CUSTOMER = 9000124405 updateContact(contact['id'], { 'contact_status_id': FORMER_CUSTOMER }) dateStr = time.ctime() note = ( f'This Contact uninstalled their WordPress plugin at [{dateStr}].') createNote('Contact', contact['id'], note) if __name__ == '__main__': # For development only. ic(findFirstCompanyWithWebsite('http://blockchainexamples.com')) #ic(findFirstCompanyWithWebsite('http://culturetv.club')) ================================================ FILE: failures-to-investigate/freshsales3.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright _!_ # # License _!_ from os.path import abspath, dirname, join as pjoin import pprint import sys import time from urllib.parse import urlparse import requests from icecream import ic _corePath = abspath(pjoin(dirname(__file__), '../')) if _corePath not in sys.path: sys.path.append(_corePath) from common.utils import lget, stripStringStart DEFAULT_FIRST_NAME = 'there' DEFAULT_LAST_NAME = '-' FRESH_SALES_API_KEY = 'P3bYheaquAHH1_hNxhMUDQ' FS_API_URL = 'https://arcindustriesinc.freshsales.io/api' FS_AUTH_HEADERS = {'Authorization': 'Token token=P3bYheaquAHH1_hNxhMUDQ'} # # FreshSales' Contact field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/contacts/fields" # # # FreshSales' Lead field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/leads/fields" # # FreshSales' Company field magic values looked up with # # curl -H "Authorization: Token token=P3bYheaquAHH1_hNxhMUDQ" -H "Content-Type: application/json" -X GET "https://arcindustriesinc.freshsales.io/api/settings/sales_accounts/fields" # def splitName(name): # Intelligently split into first and last name, if provided. # '' -> firstName: '', lastName: '-' # 'Susan' -> firstName: 'Susan', lastName: '-' # 'Greg Borp' -> firstName: 'Greg', lastName: 'Borp' # 'Freddy van der Field' -> firstName: 'Freddy', lastName: 'van der Field' toks = name.split(None, 1) firstName = lget(toks, 0, DEFAULT_FIRST_NAME) lastName = lget(toks, 1, DEFAULT_LAST_NAME) return firstName, lastName def lookupFullContact(contact): contactId = contact['id'] resp = requests.get( f'{FS_API_URL}/contacts/{contactId}?include=sales_accounts', headers=FS_AUTH_HEADERS) contact = (resp.json() or {}).get('contact') return contact def findFirstContactWithEmail(emailAddr): contacts = _findEntitiesWith('contact', 'email', emailAddr) return lget(contacts, 0) def findFirstCompanyWithWebsite(websiteUrl): ic('before', websiteUrl) if ic(urlparse(websiteUrl).scheme) is None: websiteUrl = f'http://{websiteUrl}' ic('after', websiteUrl) # FreshSales' API returns unrelated companies. For example, searching for # companies with website 'http://blockchainexamples.com', ie # # ?f=website&entities=sales_account&q=http%3A%2F%2Fblockchainexamples.com # # returns https://arcindustriesinc.freshsales.io/accounts/9001963743 with # name 'http://culturetv.club' and website # 'http://104.225.221.170:8082'. Why? Who knows. # # As a workaround, filter all returned companies to verify that the domains # match. hostNoWww = lambda url: stripStringStart(urlparse(url).hostname, 'www.') allCompanies = _findEntitiesWith('sales_account', 'website', websiteUrl) ic(allCompanies) ic(websiteUrl) companies = [ c for c in _findEntitiesWith('sales_account', 'website', websiteUrl) if hostNoWww(c.get('website')) == hostNoWww(websiteUrl)] ic(companies) firstCompany = lget(companies, 0) return firstCompany def _findEntitiesWith(entityType, query, queryValue): resp = requests.get( f'{FS_API_URL}/lookup?f={query}&entities={entityType}', params={'q': queryValue}, headers=FS_AUTH_HEADERS) entities = ( resp.json() or {}).get(f'{entityType}s', {}).get(f'{entityType}s', []) return entities def createNote(entityType, entityId, message): data = { 'note': { 'description': message, 'targetable_id': entityId, 'targetable_type': entityType, } } resp = requests.post( f'{FS_API_URL}/notes', json=data, headers=FS_AUTH_HEADERS) if resp.status_code != 201: err = f'Failed to create {entityType} note for id {entityId}.' raise RuntimeError(err) def createLead(data): return _createEntity('lead', data) def createContact(data): ANSGAR_GRUNSEID = 9000013180 data.setdefault('owner_id', ANSGAR_GRUNSEID) return _createEntity('contact', data) def createCompany(data): return _createEntity('sales_account', data) def _createEntity(entityType, data): wrapped = {entityType: data} url = f'{FS_API_URL}/{entityType}s' resp = requests.post(url, json=wrapped, headers=FS_AUTH_HEADERS) if resp.status_code not in [200, 201]: raise RuntimeError(f'Failed to create new {entityType}.') entity = (resp.json() or {}).get(entityType) return entity def updateLead(leadId, data): return _updateEntity('lead', leadId, data) def updateContact(contactId, data): return _updateEntity('contact', contactId, data) def updateCompany(companyId, data): return _updateEntity('sales_account', companyId, data) def _updateEntity(entityType, entityId, data): wrapped = {entityType: data} url = f'{FS_API_URL}/{entityType.lower()}s/{entityId}' resp = requests.put(url, json=wrapped, headers=FS_AUTH_HEADERS) if resp.status_code != 200: err = f'Failed to update {entityType.title()} with id {entityId}.' raise RuntimeError(err) entity = (resp.json() or {}).get(entityType) return entity def lookupContactsInView(viewId): return _lookupEntitiesInView('contact', viewId) def _lookupEntitiesInView(entityType, viewId): entities = [] url = f'{FS_API_URL}/{entityType.lower()}s/view/{viewId}' def pageUrl(pageNo): return url + f'?page={pageNo}' resp = requests.get(url, headers=FS_AUTH_HEADERS) js = resp.json() entities += js.get(f'{entityType}s') totalPages = js.get('meta', {}).get('total_pages') for pageNo in range(2, totalPages + 1): resp = requests.get(pageUrl(pageNo), headers=FS_AUTH_HEADERS) entities += (resp.json() or {}).get(f'{entityType}s') return entities def unsubscribeContact(contact, reasons): UNSUBSCRIBED = 9000159966 updateContact(contact['id'], { 'do_not_disturb': True, 'contact_status_id': UNSUBSCRIBED, }) dateStr = time.ctime() reasonsStr = pprint.pformat(reasons) note = ( f'This Contact unsubscribed on arc.io/unsubscribe at [{dateStr}] ' 'because:\n' '\n' f'{reasonsStr}\n' '\n') createNote('Contact', contact['id'], note) def optContactIn(contact): OPTED_IN = 9000159976 updateContact(contact['id'], { 'contact_status_id': OPTED_IN, }) def createAndOrAssociateCompanyWithContact(websiteUrl, contact): if 'sales_accounts' not in contact: contact = lookupFullContact(contact) companyToAdd = None companies = contact.get('sales_accounts', []) company = findFirstCompanyWithWebsite(websiteUrl) if company: companyId = company['id'] alreadyRelated = any(companyId == c['id'] for c in companies) if not alreadyRelated: companyToAdd = company else: companyToAdd = createCompany({ 'name': websiteUrl, 'website': websiteUrl, }) if companyToAdd: companyData = { 'id': companyToAdd['id'], # There can only be one primary Company associated with a # Contact. See https://www.freshsales.io/api/#create_contact. 'is_primary': False if companies else True, } companies.append(companyData) updateContact(contact['id'], { 'sales_accounts': companies }) return company or companyToAdd def upgradeContactWhoSubmittedSplashPage(contact, websiteUrl): createAndOrAssociateCompanyWithContact(websiteUrl, contact) SUBMITTED_ARC_IO_SIGN_UP_FORM = 9000159955 updateContact(contact['id'], { 'contact_status_id': SUBMITTED_ARC_IO_SIGN_UP_FORM, }) dateStr = time.ctime() emailAddr = contact['email'] note = ( f'This Contact submitted the sign up form on arc.io at [{dateStr}] ' f'with email address [{emailAddr}] and website [{websiteUrl}].') createNote('Contact', contact['id'], note) def noteContactSubmittedPepSplashPage(contact, websiteUrl): createAndOrAssociateCompanyWithContact(websiteUrl, contact) PEP = 9000004543 updateContact(contact['id'], { 'custom_field': { 'cf_product': 'Pep', }, }) dateStr = time.ctime() emailAddr = contact['email'] note = ( f"This Contact submitted Pep's sign up form on pep.dev at [{dateStr}] " f'with email address [{emailAddr}] and website [{websiteUrl}].') createNote('Contact', contact['id'], note) def createCrawledIndieHackersContact(name, emailAddr, websiteUrl, noteData): INDIE_HACKERS = 9000321821 _createCrawledContact(name, emailAddr, websiteUrl, INDIE_HACKERS, noteData) def _createCrawledContact(name, emailAddr, websiteUrl, leadSourceId, noteData): firstName, lastName = splitName(name) SUSPECT = 9000073090 contact = createContact({ 'email': emailAddr, 'first_name': firstName, 'last_name': lastName, 'contact_status_id': SUSPECT, 'lead_source_id': leadSourceId, }) createAndOrAssociateCompanyWithContact(websiteUrl, contact) dateStr = time.ctime() reasonsStr = pprint.pformat(noteData) note = ( f'This Contact was crawled and created on [{dateStr}]. ' 'Other data:' '\n' f'{reasonsStr}\n' '\n') createNote('Contact', contact['id'], note) def createSplashPageLead(name, emailAddr, websiteUrl): firstName, lastName = splitName(name) INTERESTED = 9000057526 ARC_IO_SIGN_UP_FORM = 9000315608 lead = createLead({ 'first_name': firstName, 'last_name': lastName, 'email': emailAddr, 'company': { 'website': websiteUrl, }, 'lead_stage_id': INTERESTED, 'lead_source_id': ARC_IO_SIGN_UP_FORM, }) dateStr = time.ctime() note = ( f'This Lead was created on [{dateStr}] because they submitted ' f'the sign up form on arc.io with email address [{emailAddr}] ' f'and website [{websiteUrl}].') createNote('Lead', lead['id'], note) def createPepSplashPageLead(emailAddr, websiteUrl): PEP = 9000004543 INTERESTED = 9000057526 PEP_SIGN_UP_FORM = 9000321929 lead = createLead({ 'first_name': DEFAULT_FIRST_NAME, 'last_name': DEFAULT_LAST_NAME, 'email': emailAddr, 'company': { 'website': websiteUrl, }, 'deal': { 'deal_product_id': PEP, }, 'lead_stage_id': INTERESTED, 'lead_source_id': PEP_SIGN_UP_FORM, }) dateStr = time.ctime() note = ( f'This Lead was created on [{dateStr}] because they submitted ' f'the sign up form on pep.dev with email address [{emailAddr}] ' f'and website [{websiteUrl}].') createNote('Lead', lead['id'], note) def noteACustomersFirstWidgetReport(emailAddr, seenOnUrl): raise NotImplementedError # TODO(grun): Finish me. contact = findFirstContactWithEmail(emailAddr) if contact: note = ( f'The widget for Arc account with email {emailAddr} was just seen ' f'live for the first seen for the first time live on {seenOnUrl}.') createNote('Contact', contact['id'], note) else: # TODO(grun): Log this scenario, which means someone added Arc's widget # to someone ic() # TODO(grun): Refactor and/or rename the below handleWordPressPlugin*() # functions, like how the above handle*() functions were refactored. def handleWordPressPluginInstall(emailAddr, websiteUrl): WORDPRESS = 9000321857 ALPHA_CODE = 9000124404 contact = findFirstContactWithEmail(emailAddr) if contact: updateContact(contact['id'], { 'lead_source_id': WORDPRESS, 'contact_status_id': ALPHA_CODE, }) else: contact = createContact({ 'email': emailAddr, 'first_name': 'there', 'last_name': websiteUrl, 'lead_source_id': WORDPRESS, 'contact_status_id': ALPHA_CODE, }) CUSTOMER = 9000095000 company = createAndOrAssociateCompanyWithContact(websiteUrl, contact) updateCompany(company['id'], { 'business_type_id': CUSTOMER, 'custom_field': { 'cf_source': 'Wordpress', }, }) dateStr = time.ctime() note = ( f"This Contact installed Arc's WordPress plugin at [{dateStr}] on " "website [{websiteUrl}].") createNote('Contact', contact['id'], note) def handleWordPressPluginCreatedArcAccount(emailAddr): contact = findFirstContactWithEmail(emailAddr) if not contact: return CUSTOMER = 9000066454 updateContact(contact['id'], { 'contact_status_id': CUSTOMER }) dateStr = time.ctime() note = ( f'This WordPress Contact created their Arc account at [{dateStr}].') createNote('Contact', contact['id'], note) def handleWordPressPluginUninstall(emailAddr): contact = findFirstContactWithEmail(emailAddr) if not contact: return FORMER_CUSTOMER = 9000124405 updateContact(contact['id'], { 'contact_status_id': FORMER_CUSTOMER }) dateStr = time.ctime() note = ( f'This Contact uninstalled their WordPress plugin at [{dateStr}].') createNote('Contact', contact['id'], note) if __name__ == '__main__': # For development only. #ic(findFirstCompanyWithWebsite('http://www.blockchainexamples.com')) #ic(findFirstCompanyWithWebsite('http://www.culturetv.club')) ic(findFirstCompanyWithWebsite('www.realizeventures.com')) ================================================ FILE: icecream/__init__.py ================================================ # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # from .icecream import * # noqa from .builtins import install, uninstall # Import all variables in __version__.py without explicit imports. from . import __version__ globals().update(dict((k, v) for k, v in __version__.__dict__.items())) ================================================ FILE: icecream/__version__.py ================================================ # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # __title__ = 'icecream' __license__ = 'MIT' __version__ = '2.1.10' __author__ = 'Ansgar Grunseid' __contact__ = 'grunseid@gmail.com' __url__ = 'https://github.com/gruns/icecream' __description__ = ( 'Never use print() to debug again: inspect variables, expressions, and ' 'program execution with a single, simple function call.') ================================================ FILE: icecream/builtins.py ================================================ # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # from typing import Optional import icecream builtins = __import__('builtins') def install( ic: str = 'ic', configured_ic: Optional[icecream.IceCreamDebugger] = None ) -> None: if configured_ic is None: configured_ic = icecream.ic setattr(builtins, ic, configured_ic) def uninstall(ic: str = 'ic') -> None: delattr(builtins, ic) ================================================ FILE: icecream/coloring.py ================================================ # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # from pygments.style import Style from pygments.token import ( Text, Name, Error, Other, String, Number, Keyword, Generic, Literal, Comment, Operator, Whitespace, Punctuation) # Solarized: https://ethanschoonover.com/solarized/ class SolarizedDark(Style): BASE03 = '#002b36' # noqa BASE02 = '#073642' # noqa BASE01 = '#586e75' # noqa BASE00 = '#657b83' # noqa BASE0 = '#839496' # noqa BASE1 = '#93a1a1' # noqa BASE2 = '#eee8d5' # noqa BASE3 = '#fdf6e3' # noqa YELLOW = '#b58900' # noqa ORANGE = '#cb4b16' # noqa RED = '#dc322f' # noqa MAGENTA = '#d33682' # noqa VIOLET = '#6c71c4' # noqa BLUE = '#268bd2' # noqa CYAN = '#2aa198' # noqa GREEN = '#859900' # noqa styles = { Text: BASE0, Whitespace: BASE03, Error: RED, Other: BASE0, Name: BASE1, Name.Attribute: BASE0, Name.Builtin: BLUE, Name.Builtin.Pseudo: BLUE, Name.Class: BLUE, Name.Constant: YELLOW, Name.Decorator: ORANGE, Name.Entity: ORANGE, Name.Exception: ORANGE, Name.Function: BLUE, Name.Property: BLUE, Name.Label: BASE0, Name.Namespace: YELLOW, Name.Other: BASE0, Name.Tag: GREEN, Name.Variable: ORANGE, Name.Variable.Class: BLUE, Name.Variable.Global: BLUE, Name.Variable.Instance: BLUE, String: CYAN, String.Backtick: CYAN, String.Char: CYAN, String.Doc: CYAN, String.Double: CYAN, String.Escape: ORANGE, String.Heredoc: CYAN, String.Interpol: ORANGE, String.Other: CYAN, String.Regex: CYAN, String.Single: CYAN, String.Symbol: CYAN, Number: CYAN, Number.Float: CYAN, Number.Hex: CYAN, Number.Integer: CYAN, Number.Integer.Long: CYAN, Number.Oct: CYAN, Keyword: GREEN, Keyword.Constant: GREEN, Keyword.Declaration: GREEN, Keyword.Namespace: ORANGE, Keyword.Pseudo: ORANGE, Keyword.Reserved: GREEN, Keyword.Type: GREEN, Generic: BASE0, Generic.Deleted: BASE0, Generic.Emph: BASE0, Generic.Error: BASE0, Generic.Heading: BASE0, Generic.Inserted: BASE0, Generic.Output: BASE0, Generic.Prompt: BASE0, Generic.Strong: BASE0, Generic.Subheading: BASE0, Generic.Traceback: BASE0, Literal: BASE0, Literal.Date: BASE0, Comment: BASE01, Comment.Multiline: BASE01, Comment.Preproc: BASE01, Comment.Single: BASE01, Comment.Special: BASE01, Operator: BASE0, Operator.Word: GREEN, Punctuation: BASE0, } ================================================ FILE: icecream/icecream.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # import ast import enum import inspect import pprint import sys from types import FrameType from typing import Optional, cast, Any, Callable, Generator, List, Sequence, Tuple, Type, Union, cast, Literal import warnings from datetime import datetime import functools from contextlib import contextmanager from os.path import basename, realpath from textwrap import dedent import colorama import executing from pygments import highlight # See https://gist.github.com/XVilka/8346728 for color support in various # terminals and thus whether to use Terminal256Formatter or # TerminalTrueColorFormatter. from pygments.formatters import Terminal256Formatter from pygments.lexers import PythonLexer as PyLexer, Python3Lexer as Py3Lexer from .coloring import SolarizedDark class Sentinel(enum.Enum): absent = object() def bindStaticVariable(name: str, value: Any) -> Callable: def decorator(fn: Callable) -> Callable: setattr(fn, name, value) return fn return decorator @bindStaticVariable('formatter', Terminal256Formatter(style=SolarizedDark)) @bindStaticVariable('lexer', Py3Lexer(ensurenl=False)) def colorize(s: str) -> str: self = colorize return highlight( s, cast(Py3Lexer, self.lexer), cast(Terminal256Formatter, self.formatter) ) # pyright: ignore[reportFunctionMemberAccess] @contextmanager def supportTerminalColorsInWindows() -> Generator: # Filter and replace ANSI escape sequences on Windows with equivalent Win32 # API calls. This code does nothing on non-Windows systems. if sys.platform.startswith('win'): colorama.init() yield colorama.deinit() else: yield def stderrPrint(*args: object) -> None: print(*args, file=sys.stderr) def isLiteral(s: str) -> bool: try: ast.literal_eval(s) except Exception: return False return True def colorizedStderrPrint(s: str) -> None: colored = colorize(s) with supportTerminalColorsInWindows(): stderrPrint(colored) def colorizedStdoutPrint(s: str) -> None: colored = colorize(s) with supportTerminalColorsInWindows(): print(colored) def safe_pformat(obj: object, *args: Any, **kwargs: Any) -> str: """pprint.pformat() with a couple of small safety/usability tweaks. In addition to the usual TypeError handling below, we special–case "medium sized" flat lists. For those, the standard pprint heuristics sometimes choose a one-item-per-line layout which makes the order of values hard to visually follow in ic()'s output. For such lists we prefer the more compact repr()-style representation. """ def _pformat(extra_kwargs: Optional[dict] = None) -> str: # Helper so we always pass the same args/kwargs to pprint. final_kwargs = dict(kwargs) if extra_kwargs: final_kwargs.update(extra_kwargs) return pprint.pformat(obj, *args, **final_kwargs) try: # For flat lists we try a slightly wider layout first. This keeps # simple medium-sized lists on a single line in the common case. is_flat_list = ( isinstance(obj, list) and not args and 'width' not in kwargs and not any(isinstance(el, (list, tuple, dict, set)) for el in obj) ) if is_flat_list: formatted = _pformat({'width': 120}) else: formatted = _pformat(None) except TypeError as e: # Sorting likely tripped on symbolic/elementwise comparisons. warnings.warn(f"pprint failed ({e}); retrying without dict sorting") try: # Py 3.8+: disable sorting globally for all nested dicts. return _pformat({'sort_dicts': False}) except TypeError: # Py < 3.8: last-ditch, always works. return repr(obj) # Heuristic: if pprint decided to break a flat, medium-sized list across # many lines, fall back to repr() which keeps the list visually compact # and easier to read in ic()'s prefix/value layout. if is_flat_list and isinstance(obj, list) and 13 <= len(obj) <= 35: lines = formatted.splitlines() if len(lines) > 10: one_line = repr(obj) if len(one_line) <= 120: return one_line return formatted DEFAULT_PREFIX = 'ic| ' DEFAULT_LINE_WRAP_WIDTH = 70 # Characters. DEFAULT_CONTEXT_DELIMITER = '- ' DEFAULT_OUTPUT_FUNCTION = colorizedStderrPrint DEFAULT_ARG_TO_STRING_FUNCTION = safe_pformat """ This info message is printed instead of the arguments when icecream fails to find or access source code that's required to parse and analyze. This can happen, for example, when - ic() is invoked inside a REPL or interactive shell, e.g. from the command line (CLI) or with python -i. - The source code is mangled and/or packaged, e.g. with a project freezer like PyInstaller. - The underlying source code changed during execution. See https://stackoverflow.com/a/33175832. """ NO_SOURCE_AVAILABLE_WARNING_MESSAGE = ( 'Failed to access the underlying source code for analysis. Was ic() ' 'invoked in a REPL (e.g. from the command line), a frozen application ' '(e.g. packaged with PyInstaller), or did the underlying source code ' 'change during execution?') def callOrValue(obj: object) -> object: return obj() if callable(obj) else obj class Source(executing.Source): def get_text_with_indentation(self, node: ast.expr) -> str: result = self.asttokens().get_text(node) if '\n' in result: result = ' ' * node.first_token.start[1] + result # type: ignore[attr-defined] result = dedent(result) result = result.strip() return result def prefixLines(prefix: str, s: str, startAtLine: int = 0) -> List[str]: lines = s.splitlines() for i in range(startAtLine, len(lines)): lines[i] = prefix + lines[i] return lines def prefixFirstLineIndentRemaining(prefix: str, s: str) -> List[str]: indent = ' ' * len(prefix) lines = prefixLines(indent, s, startAtLine=1) lines[0] = prefix + lines[0] return lines def formatPair(prefix: str, arg: Union[str, Sentinel], value: str) -> str: if arg is Sentinel.absent: argLines = [] valuePrefix = prefix else: argLines = prefixFirstLineIndentRemaining(prefix, arg) valuePrefix = argLines[-1] + ': ' looksLikeAString = (value[0] + value[-1]) in ["''", '""'] if looksLikeAString: # Align the start of multiline strings. valueLines = prefixLines(' ', value, startAtLine=1) value = '\n'.join(valueLines) valueLines = prefixFirstLineIndentRemaining(valuePrefix, value) lines = argLines[:-1] + valueLines return '\n'.join(lines) class _SingleDispatchCallable: def __call__(self, *_: object) -> str: # This is a marker class, not a real thing you should use raise NotImplementedError register: Callable[[Type], Callable] def singledispatch(func: Callable) -> _SingleDispatchCallable: func = functools.singledispatch(func) # add unregister based on https://stackoverflow.com/a/25951784 assert func.register.__closure__ is not None closure = dict(zip(func.register.__code__.co_freevars, func.register.__closure__)) registry = closure['registry'].cell_contents dispatch_cache = closure['dispatch_cache'].cell_contents def unregister(cls: Type) -> None: del registry[cls] dispatch_cache.clear() func.unregister = unregister # type: ignore[attr-defined] return cast(_SingleDispatchCallable, func) @singledispatch def argumentToString(obj: object) -> str: s = DEFAULT_ARG_TO_STRING_FUNCTION(obj) s = s.replace('\\n', '\n') # Preserve string newlines in output. return s @argumentToString.register(str) def _(obj: str) -> str: if '\n' in obj: return "'''" + obj + "'''" return "'" + obj.replace('\\', '\\\\') + "'" class IceCreamDebugger: _pairDelimiter = ', ' # Used by the tests in tests/. lineWrapWidth = DEFAULT_LINE_WRAP_WIDTH contextDelimiter = DEFAULT_CONTEXT_DELIMITER def __init__(self, prefix: Union[str, Callable[[], str]] =DEFAULT_PREFIX, outputFunction: Callable[[str], None]=DEFAULT_OUTPUT_FUNCTION, argToStringFunction: Union[_SingleDispatchCallable, Callable[[Any], str]]=argumentToString, includeContext: bool=False, contextAbsPath: bool=False): self.enabled = True self.prefix = prefix self.includeContext = includeContext self.outputFunction = outputFunction self.argToStringFunction = argToStringFunction self.contextAbsPath = contextAbsPath def __call__(self, *args: object) -> object: if self.enabled: currentFrame = inspect.currentframe() assert currentFrame is not None and currentFrame.f_back is not None callFrame = currentFrame.f_back self.outputFunction(self._format(callFrame, *args)) if not args: # E.g. ic(). passthrough = None elif len(args) == 1: # E.g. ic(1). passthrough = args[0] else: # E.g. ic(1, 2, 3). passthrough = args return passthrough def format(self, *args: object) -> str: currentFrame = inspect.currentframe() assert currentFrame is not None and currentFrame.f_back is not None callFrame = currentFrame.f_back out = self._format(callFrame, *args) return out def _format(self, callFrame: FrameType, *args: object) -> str: prefix = cast(str, callOrValue(self.prefix)) context = self._formatContext(callFrame) if not args: time = self._formatTime() out = prefix + context + time else: if not self.includeContext: context = '' out = self._formatArgs( callFrame, prefix, context, args) return out def _formatArgs(self, callFrame: FrameType, prefix: str, context: str, args: Sequence[object]) -> str: callNode = Source.executing(callFrame).node if callNode is not None: assert isinstance(callNode, ast.Call) source = cast(Source, Source.for_frame(callFrame)) sanitizedArgStrs = [ source.get_text_with_indentation(arg) for arg in callNode.args] else: warnings.warn( NO_SOURCE_AVAILABLE_WARNING_MESSAGE, category=RuntimeWarning, stacklevel=4) sanitizedArgStrs = [Sentinel.absent] * len(args) pairs = list(zip(sanitizedArgStrs, cast(List[str], args))) out = self._constructArgumentOutput(prefix, context, pairs) return out def _constructArgumentOutput(self, prefix: str, context: str, pairs: Sequence[Tuple[Union[str, Sentinel], str]]) -> str: def argPrefix(arg: str) -> str: return '%s: ' % arg pairs = [(arg, self.argToStringFunction(val)) for arg, val in pairs] # For cleaner output, if is a literal, eg 3, "a string", # b'bytes', etc, only output the value, not the argument and the # value, because the argument and the value will be identical or # nigh identical. Ex: with ic("hello"), just output # # ic| 'hello', # # instead of # # ic| "hello": 'hello'. # # When the source for an arg is missing we also only print the value, # since we can't know anything about the argument itself. pairStrs = [ val if (arg is Sentinel.absent or isLiteral(arg)) else (argPrefix(arg) + val) for arg, val in pairs] allArgsOnOneLine = self._pairDelimiter.join(pairStrs) multilineArgs = len(allArgsOnOneLine.splitlines()) > 1 contextDelimiter = self.contextDelimiter if context else '' allPairs = prefix + context + contextDelimiter + allArgsOnOneLine firstLineTooLong = len(allPairs.splitlines()[0]) > self.lineWrapWidth if multilineArgs or firstLineTooLong: # ic| foo.py:11 in foo() # multilineStr: 'line1 # line2' # # ic| foo.py:11 in foo() # a: 11111111111111111111 # b: 22222222222222222222 if context: lines = [prefix + context] + [ formatPair(len(prefix) * ' ', arg, value) for arg, value in pairs ] # ic| multilineStr: 'line1 # line2' # # ic| a: 11111111111111111111 # b: 22222222222222222222 else: argLines = [ formatPair('', arg, value) for arg, value in pairs ] lines = prefixFirstLineIndentRemaining(prefix, '\n'.join(argLines)) # ic| foo.py:11 in foo()- a: 1, b: 2 # ic| a: 1, b: 2, c: 3 else: lines = [prefix + context + contextDelimiter + allArgsOnOneLine] return '\n'.join(lines) def _formatContext(self, callFrame: FrameType) -> str: filename, lineNumber, parentFunction = self._getContext(callFrame) if parentFunction != '': parentFunction = '%s()' % parentFunction context = '%s:%s in %s' % (filename, lineNumber, parentFunction) return context def _formatTime(self) -> str: now = datetime.now() formatted = now.strftime('%H:%M:%S.%f')[:-3] return ' at %s' % formatted def _getContext(self, callFrame: FrameType) -> Tuple[str, int, str]: frameInfo = inspect.getframeinfo(callFrame) lineNumber = frameInfo.lineno parentFunction = frameInfo.function filepath = (realpath if self.contextAbsPath else basename)(frameInfo.filename) # type: ignore[operator] return filepath, lineNumber, parentFunction def enable(self) -> None: self.enabled = True def disable(self) -> None: self.enabled = False def use_stdout(self) -> None: self.outputFunction = colorizedStdoutPrint def use_stderr(self) -> None: self.outputFunction = colorizedStderrPrint def configureOutput( self: "IceCreamDebugger", prefix: Union[str, Literal[Sentinel.absent]] = Sentinel.absent, outputFunction: Union[Callable, Literal[Sentinel.absent]] = Sentinel.absent, argToStringFunction: Union[Callable, Literal[Sentinel.absent]] = Sentinel.absent, includeContext: Union[bool, Literal[Sentinel.absent]] = Sentinel.absent, contextAbsPath: Union[bool, Literal[Sentinel.absent]] = Sentinel.absent, lineWrapWidth: Union[bool, Literal[Sentinel.absent]] = Sentinel.absent ) -> None: noParameterProvided = all( v is Sentinel.absent for k, v in locals().items() if k != 'self') if noParameterProvided: raise TypeError('configureOutput() missing at least one argument') if prefix is not Sentinel.absent: self.prefix = prefix if outputFunction is not Sentinel.absent: self.outputFunction = outputFunction if argToStringFunction is not Sentinel.absent: self.argToStringFunction = argToStringFunction if includeContext is not Sentinel.absent: self.includeContext = includeContext if contextAbsPath is not Sentinel.absent: self.contextAbsPath = contextAbsPath if lineWrapWidth is not Sentinel.absent: self.lineWrapWidth = lineWrapWidth ic = IceCreamDebugger() ================================================ FILE: icecream/py.typed ================================================ ================================================ FILE: pyproject.toml ================================================ [tool.mypy] show_error_codes=true disallow_untyped_defs=true disallow_untyped_calls=true warn_redundant_casts=true ================================================ FILE: setup.cfg ================================================ [metadata] license_files = LICENSE.txt ================================================ FILE: setup.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # import os import sys from os.path import dirname, join as pjoin from setuptools import setup, find_packages, Command from setuptools.command.test import test as TestCommand meta = {} with open(pjoin('icecream', '__version__.py')) as f: exec(f.read(), meta) class Publish(Command): """Publish to PyPI with twine.""" user_options = [] def initialize_options(self) -> None: pass def finalize_options(self) -> None: pass def run(self) -> None: os.system('python3 setup.py sdist bdist_wheel') sdist = 'dist/icecream-%s.tar.gz' % meta['__version__'] wheel = 'dist/icecream-%s-py3-none-any.whl' % meta['__version__'] rc = os.system('twine upload "%s" "%s"' % (sdist, wheel)) sys.exit(rc) class RunTests(TestCommand): """ Run the unit tests. By default, `python setup.py test` fails if tests/ isn't a Python module (that is, if the tests/ directory doesn't contain an __init__.py file). But the tests/ directory shouldn't contain an __init__.py file and tests/ shouldn't be a Python module. See http://doc.pytest.org/en/latest/goodpractices.html Running the unit tests manually here enables `python setup.py test` without tests/ being a Python module. """ def run_tests(self) -> None: from unittest import TestLoader, TextTestRunner tests_dir = pjoin(dirname(__file__), 'tests') suite = TestLoader().discover(tests_dir) result = TextTestRunner().run(suite) sys.exit(0 if result.wasSuccessful() else -1) setup( name=meta['__title__'], license=meta['__license__'], version=meta['__version__'], author=meta['__author__'], author_email=meta['__contact__'], url=meta['__url__'], description=meta['__description__'], long_description=( 'Information and documentation can be found at ' 'https://github.com/gruns/icecream.'), platforms=['any'], packages=find_packages(exclude=['tests', 'tests.*']), include_package_data=True, package_data={'icecream': ['py.typed']}, classifiers=[ 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: CPython', ], tests_require=[ 'tox>=4', ], install_requires=[ 'colorama>=0.3.9', 'pygments>=2.2.0', 'executing>=2.1.0', 'asttokens>=2.0.1', ], cmdclass={ 'test': RunTests, 'publish': Publish, }, ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/install_test_import.py ================================================ # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # def runMe(): x = 3 ic(x) ================================================ FILE: tests/test_icecream.py ================================================ # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # import sys import unittest import warnings from io import StringIO from contextlib import contextmanager from os.path import basename, splitext, realpath import icecream from icecream import ic, argumentToString, stderrPrint, NO_SOURCE_AVAILABLE_WARNING_MESSAGE TEST_PAIR_DELIMITER = '| ' MY_FILENAME = basename(__file__) MY_FILEPATH = realpath(__file__) a = 1 b = 2 c = 3 def noop(*args, **kwargs): return def has_ansi_escape_codes(s): # Oversimplified, but ¯\_(ツ)_/¯. TODO(grun): Test with regex. return '\x1b[' in s class FakeTeletypeBuffer(StringIO): """ Extend StringIO to act like a TTY so ANSI control codes aren't stripped when wrapped with colorama's wrap_stream(). """ def isatty(self): return True @contextmanager def disable_coloring(): originalOutputFunction = ic.outputFunction ic.configureOutput(outputFunction=stderrPrint) yield ic.configureOutput(outputFunction=originalOutputFunction) @contextmanager def configure_icecream_output(prefix=None, outputFunction=None, argToStringFunction=None, includeContext=None, contextAbsPath=None): oldPrefix = ic.prefix oldOutputFunction = ic.outputFunction oldArgToStringFunction = ic.argToStringFunction oldIncludeContext = ic.includeContext oldContextAbsPath = ic.contextAbsPath if prefix: ic.configureOutput(prefix=prefix) if outputFunction: ic.configureOutput(outputFunction=outputFunction) if argToStringFunction: ic.configureOutput(argToStringFunction=argToStringFunction) if includeContext: ic.configureOutput(includeContext=includeContext) if contextAbsPath: ic.configureOutput(contextAbsPath=contextAbsPath) yield ic.configureOutput( oldPrefix, oldOutputFunction, oldArgToStringFunction, oldIncludeContext, oldContextAbsPath) @contextmanager def capture_standard_streams(): realStdout = sys.stdout realStderr = sys.stderr newStdout = FakeTeletypeBuffer() newStderr = FakeTeletypeBuffer() try: sys.stdout = newStdout sys.stderr = newStderr yield newStdout, newStderr finally: sys.stdout = realStdout sys.stderr = realStderr def strip_prefix(line): if line.startswith(ic.prefix): line = line.strip()[len(ic.prefix):] return line def line_is_context_and_time(line): line = strip_prefix(line) # ic| f.py:33 in foo() at 08:08:51.389 context, time = line.split(' at ') return ( line_is_context(context) and len(time.split(':')) == 3 and len(time.split('.')) == 2) def line_is_context(line): line = strip_prefix(line) # ic| f.py:33 in foo() sourceLocation, function = line.split(' in ') # f.py:33 in foo() filename, lineNumber = sourceLocation.split(':') # f.py:33 name, ext = splitext(filename) return ( int(lineNumber) > 0 and ext in ['.py', '.pyc', '.pyo'] and name == splitext(MY_FILENAME)[0] and (function == '' or function.endswith('()'))) def line_is_abs_path_context(line): line = strip_prefix(line) # ic| /absolute/path/to/f.py:33 in foo() sourceLocation, function = line.split(' in ') # /absolute/path/to/f.py:33 in foo() filepath, lineNumber = sourceLocation.split(':') # /absolute/path/to/f.py:33 path, ext = splitext(filepath) return ( int(lineNumber) > 0 and ext in ['.py', '.pyc', '.pyo'] and path == splitext(MY_FILEPATH)[0] and (function == '' or function.endswith('()'))) def line_after_context(line, prefix): if line.startswith(prefix): line = line[len(prefix):] toks = line.split(' in ', 1) if len(toks) == 2: rest = toks[1].split(' ') line = ' '.join(rest[1:]) return line def parse_output_into_pairs(out, err, assert_num_lines, prefix=icecream.DEFAULT_PREFIX): if isinstance(out, StringIO): out = out.getvalue() if isinstance(err, StringIO): err = err.getvalue() assert not out lines = err.splitlines() if assert_num_lines: assert len(lines) == assert_num_lines line_pairs = [] for line in lines: line = line_after_context(line, prefix) if not line: line_pairs.append([]) continue pairStrs = line.split(TEST_PAIR_DELIMITER) pairs = [tuple(s.split(':', 1)) for s in pairStrs] # Indented line of a multiline value. if len(pairs[0]) == 1 and line.startswith(' '): arg, value = line_pairs[-1][-1] looksLikeAString = value[0] in ["'", '"'] prefix = ((arg + ': ' if arg is not None else '') # A multiline value + (' ' if looksLikeAString else '')) dedented = line[len(ic.prefix) + len(prefix):] line_pairs[-1][-1] = (arg, value + '\n' + dedented) else: items = [ (None, p[0].strip()) if len(p) == 1 # A value, like ic(3). else (p[0].strip(), p[1].strip()) # A variable, like ic(a). for p in pairs] line_pairs.append(items) return line_pairs class TestIceCream(unittest.TestCase): def setUp(self): ic._pairDelimiter = TEST_PAIR_DELIMITER def test_metadata(self): def is_non_empty_string(s): return isinstance(s, str) and s assert is_non_empty_string(icecream.__title__) assert is_non_empty_string(icecream.__version__) assert is_non_empty_string(icecream.__license__) assert is_non_empty_string(icecream.__author__) assert is_non_empty_string(icecream.__contact__) assert is_non_empty_string(icecream.__description__) assert is_non_empty_string(icecream.__url__) def test_without_args(self): with disable_coloring(), capture_standard_streams() as (out, err): ic() assert line_is_context_and_time(err.getvalue()) def test_as_argument(self): with disable_coloring(), capture_standard_streams() as (out, err): noop(ic(a), ic(b)) pairs = parse_output_into_pairs(out, err, 2) assert pairs[0][0] == ('a', '1') and pairs[1][0] == ('b', '2') with disable_coloring(), capture_standard_streams() as (out, err): dic = {1: ic(a)} # noqa lst = [ic(b), ic()] # noqa pairs = parse_output_into_pairs(out, err, 3) assert pairs[0][0] == ('a', '1') assert pairs[1][0] == ('b', '2') assert line_is_context_and_time(err.getvalue().splitlines()[-1]) def test_single_argument(self): with disable_coloring(), capture_standard_streams() as (out, err): ic(a) assert parse_output_into_pairs(out, err, 1)[0][0] == ('a', '1') def test_multiple_arguments(self): with disable_coloring(), capture_standard_streams() as (out, err): ic(a, b) pairs = parse_output_into_pairs(out, err, 1)[0] assert pairs == [('a', '1'), ('b', '2')] def test_nested_multiline(self): with disable_coloring(), capture_standard_streams() as (out, err): ic( ) assert line_is_context_and_time(err.getvalue()) with disable_coloring(), capture_standard_streams() as (out, err): ic(a, 'foo') pairs = parse_output_into_pairs(out, err, 1)[0] assert pairs == [('a', '1'), (None, "'foo'")] with disable_coloring(), capture_standard_streams() as (out, err): noop(noop(noop({1: ic( noop())}))) assert parse_output_into_pairs(out, err, 1)[0][0] == ('noop()', 'None') def test_expression_arguments(self): class klass(): attr = 'yep' d = {'d': {1: 'one'}, 'k': klass} with disable_coloring(), capture_standard_streams() as (out, err): ic(d['d'][1]) pair = parse_output_into_pairs(out, err, 1)[0][0] assert pair == ("d['d'][1]", "'one'") with disable_coloring(), capture_standard_streams() as (out, err): ic(d['k'].attr) pair = parse_output_into_pairs(out, err, 1)[0][0] assert pair == ("d['k'].attr", "'yep'") def test_multiple_calls_on_same_line(self): with disable_coloring(), capture_standard_streams() as (out, err): ic(a); ic(b, c) # noqa pairs = parse_output_into_pairs(out, err, 2) assert pairs[0][0] == ('a', '1') assert pairs[1] == [('b', '2'), ('c', '3')] def test_call_surrounded_by_expressions(self): with disable_coloring(), capture_standard_streams() as (out, err): noop(); ic(a); noop() # noqa assert parse_output_into_pairs(out, err, 1)[0][0] == ('a', '1') def test_comments(self): with disable_coloring(), capture_standard_streams() as (out, err): """Comment."""; ic(); # Comment. # noqa assert line_is_context_and_time(err.getvalue()) def test_method_arguments(self): class Foo: def foo(self): return 'foo' f = Foo() with disable_coloring(), capture_standard_streams() as (out, err): ic(f.foo()) assert parse_output_into_pairs(out, err, 1)[0][0] == ('f.foo()', "'foo'") def test_complicated(self): with disable_coloring(), capture_standard_streams() as (out, err): noop(); ic(); noop(); ic(a, # noqa b, noop.__class__.__name__, # noqa noop ()); noop() # noqa pairs = parse_output_into_pairs(out, err, 2) assert line_is_context_and_time(err.getvalue().splitlines()[0]) assert pairs[1] == [ ('a', '1'), ('b', '2'), ('noop.__class__.__name__', "'function'"), ('noop ()', 'None')] def test_return_value(self): with disable_coloring(), capture_standard_streams() as (out, err): assert ic() is None assert ic(1) == 1 assert ic(1, 2, 3) == (1, 2, 3) def test_different_name(self): from icecream import ic as foo with disable_coloring(), capture_standard_streams() as (out, err): foo() assert line_is_context_and_time(err.getvalue()) newname = foo with disable_coloring(), capture_standard_streams() as (out, err): newname(a) pair = parse_output_into_pairs(out, err, 1)[0][0] assert pair == ('a', '1') def test_prefix_configuration(self): prefix = 'lolsup ' with configure_icecream_output(prefix, stderrPrint): with disable_coloring(), capture_standard_streams() as (out, err): ic(a) pair = parse_output_into_pairs(out, err, 1, prefix=prefix)[0][0] assert pair == ('a', '1') def prefix_function(): return 'lolsup ' with configure_icecream_output(prefix=prefix_function): with disable_coloring(), capture_standard_streams() as (out, err): ic(b) pair = parse_output_into_pairs(out, err, 1, prefix=prefix_function())[0][0] assert pair == ('b', '2') def test_output_function(self): lst = [] def append_to(s): lst.append(s) with configure_icecream_output(ic.prefix, append_to): with capture_standard_streams() as (out, err): ic(a) assert not out.getvalue() and not err.getvalue() with configure_icecream_output(outputFunction=append_to): with capture_standard_streams() as (out, err): ic(b) assert not out.getvalue() and not err.getvalue() pairs = parse_output_into_pairs(out, '\n'.join(lst), 2) assert pairs == [[('a', '1')], [('b', '2')]] def test_enable_disable(self): with disable_coloring(), capture_standard_streams() as (out, err): assert ic(a) == 1 assert ic.enabled ic.disable() assert not ic.enabled assert ic(b) == 2 ic.enable() assert ic.enabled assert ic(c) == 3 pairs = parse_output_into_pairs(out, err, 2) assert pairs == [[('a', '1')], [('c', '3')]] def test_arg_to_string_function(self): def hello(obj): return 'zwei' with configure_icecream_output(argToStringFunction=hello): with disable_coloring(), capture_standard_streams() as (out, err): eins = 'ein' ic(eins) pair = parse_output_into_pairs(out, err, 1)[0][0] assert pair == ('eins', 'zwei') def test_singledispatch_argument_to_string(self): def argument_to_string_tuple(obj): return "Dispatching tuple!" # Prepare input and output x = (1, 2) default_output = ic.format(x) # Register argumentToString.register(tuple, argument_to_string_tuple) assert tuple in argumentToString.registry assert str.endswith(ic.format(x), argument_to_string_tuple(x)) # Unregister argumentToString.unregister(tuple) assert tuple not in argumentToString.registry assert ic.format(x) == default_output def test_single_argument_long_line_not_wrapped(self): # A single long line with one argument is not line wrapped. longStr = '*' * (ic.lineWrapWidth + 1) with disable_coloring(), capture_standard_streams() as (out, err): ic(longStr) pair = parse_output_into_pairs(out, err, 1)[0][0] assert len(err.getvalue()) > ic.lineWrapWidth assert pair == ('longStr', ic.argToStringFunction(longStr)) def test_multiple_arguments_long_line_wrapped(self): # A single long line with multiple variables is line wrapped. val = '*' * int(ic.lineWrapWidth / 4) valStr = ic.argToStringFunction(val) v1 = v2 = v3 = v4 = val with disable_coloring(), capture_standard_streams() as (out, err): ic(v1, v2, v3, v4) pairs = parse_output_into_pairs(out, err, 4) assert pairs == [[(k, valStr)] for k in ['v1', 'v2', 'v3', 'v4']] lines = err.getvalue().splitlines() assert ( lines[0].startswith(ic.prefix) and lines[1].startswith(' ' * len(ic.prefix)) and lines[2].startswith(' ' * len(ic.prefix)) and lines[3].startswith(' ' * len(ic.prefix))) def test_multiline_value_wrapped(self): # Multiline values are line wrapped. multilineStr = 'line1\nline2' with disable_coloring(), capture_standard_streams() as (out, err): ic(multilineStr) pair = parse_output_into_pairs(out, err, 2)[0][0] assert pair == ('multilineStr', ic.argToStringFunction(multilineStr)) def test_include_context_single_line(self): i = 3 with configure_icecream_output(includeContext=True): with disable_coloring(), capture_standard_streams() as (out, err): ic(i) pair = parse_output_into_pairs(out, err, 1)[0][0] assert pair == ('i', '3') def test_context_abs_path_single_line(self): i = 3 with configure_icecream_output(includeContext=True, contextAbsPath=True): with disable_coloring(), capture_standard_streams() as (out, err): ic(i) # Output with absolute path can easily exceed line width, so no assert line num here. pairs = parse_output_into_pairs(out, err, 0) assert [('i', '3')] in pairs def test_values(self): with disable_coloring(), capture_standard_streams() as (out, err): # Test both 'asdf' and "asdf"; see # https://github.com/gruns/icecream/issues/53. ic(3, 'asdf', "asdf") pairs = parse_output_into_pairs(out, err, 1) assert pairs == [[(None, '3'), (None, "'asdf'"), (None, "'asdf'")]] def test_include_context_multi_line(self): multilineStr = 'line1\nline2' with configure_icecream_output(includeContext=True): with disable_coloring(), capture_standard_streams() as (out, err): ic(multilineStr) firstLine = err.getvalue().splitlines()[0] assert line_is_context(firstLine) pair = parse_output_into_pairs(out, err, 3)[1][0] assert pair == ('multilineStr', ic.argToStringFunction(multilineStr)) def test_context_abs_path_multi_line(self): multilineStr = 'line1\nline2' with configure_icecream_output(includeContext=True, contextAbsPath=True): with disable_coloring(), capture_standard_streams() as (out, err): ic(multilineStr) firstLine = err.getvalue().splitlines()[0] assert line_is_abs_path_context(firstLine) pair = parse_output_into_pairs(out, err, 3)[1][0] assert pair == ('multilineStr', ic.argToStringFunction(multilineStr)) def test_format(self): with disable_coloring(), capture_standard_streams() as (out, err): """comment"""; noop(); ic( # noqa 'sup'); noop() # noqa """comment"""; noop(); s = ic.format( # noqa 'sup'); noop() # noqa assert s == err.getvalue().rstrip() def test_multiline_invocation_with_comments(self): with disable_coloring(), capture_standard_streams() as (out, err): ic( # Comment. a, # Comment. # Comment. b, # Comment. ) # Comment. pairs = parse_output_into_pairs(out, err, 1)[0] assert pairs == [('a', '1'), ('b', '2')] def test_no_source_available_prints_values(self): with disable_coloring(), capture_standard_streams() as (out, err): with warnings.catch_warnings(): # we ignore the warning so that it doesn't interfere # with parsing ic's output warnings.simplefilter("ignore") eval('ic(a, b)') pairs = parse_output_into_pairs(out, err, 1) self.assertEqual(pairs, [[(None, '1'), (None, "2")]]) def test_no_source_available_prints_multiline(self): """ This tests for a bug which caused only multiline prints to fail. """ multilineStr = 'line1\nline2' with disable_coloring(), capture_standard_streams() as (out, err): with warnings.catch_warnings(): # we ignore the warning so that it doesn't interfere # with parsing ic's output warnings.simplefilter("ignore") eval('ic(multilineStr)') pair = parse_output_into_pairs(out, err, 2)[0][0] self.assertEqual(pair, (None, ic.argToStringFunction(multilineStr))) def test_no_source_available_issues_exactly_one_warning(self): with disable_coloring(), capture_standard_streams() as (out, err): with warnings.catch_warnings(record=True) as all_warnings: eval('ic(a)') eval('ic(b)') assert len(all_warnings) == 1 warning = all_warnings[-1] assert NO_SOURCE_AVAILABLE_WARNING_MESSAGE in str(warning.message) def test_single_tuple_argument(self): with disable_coloring(), capture_standard_streams() as (out, err): ic((a, b)) pair = parse_output_into_pairs(out, err, 1)[0][0] self.assertEqual(pair, ('(a, b)', '(1, 2)')) def test_flat_medium_list_prints_on_one_line(self): """Flat medium-sized lists should not be split one item per line.""" data = [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0] with disable_coloring(), capture_standard_streams() as (out, err): ic(data) # The whole ic() call should fit on a single line. self.assertEqual(len(err.getvalue().strip().splitlines()), 1) def test_multiline_container_args(self): with disable_coloring(), capture_standard_streams() as (out, err): ic((a, b)) ic([a, b]) ic((a, b), [list(range(15)), list(range(15))]) self.assertEqual(err.getvalue().strip(), """ ic| (a, b): (1, 2) ic| [a, b]: [1, 2] ic| (a, b): (1, 2) [list(range(15)), list(range(15))]: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]] """.strip()) with disable_coloring(), capture_standard_streams() as (out, err): with configure_icecream_output(includeContext=True): ic((a, b), [list(range(15)), list(range(15))]) lines = err.getvalue().strip().splitlines() self.assertRegex( lines[0], r'ic\| test_icecream.py:\d+ in test_multiline_container_args\(\)', ) self.assertEqual('\n'.join(lines[1:]), """\ (a, b): (1, 2) [list(range(15)), list(range(15))]: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]]""") def test_multiple_tuple_arguments(self): with disable_coloring(), capture_standard_streams() as (out, err): ic((a, b), (b, a), a, b) pair = parse_output_into_pairs(out, err, 1)[0] self.assertEqual(pair, [ ('(a, b)', '(1, 2)'), ('(b, a)', '(2, 1)'), ('a', '1'), ('b', '2')]) def test_coloring(self): with capture_standard_streams() as (out, err): ic({1: 'str'}) # Output should be colored with ANSI control codes. assert has_ansi_escape_codes(err.getvalue()) def test_configure_output_with_no_parameters(self): with self.assertRaises(TypeError): ic.configureOutput() def test_multiline_strings_output(self): test1 = "A\\veryvery\\long\\path\\to\\no\\even\\longer\\HelloWorld _01_Heritisfinallythe file.file" test2 = r"A\veryvery\long\path\to\no\even\longer\HelloWorld _01_Heritisfinallythe file.file" test3 = "line\nline" with disable_coloring(), capture_standard_streams() as (_, err): ic(test1) curr_res = err.getvalue().strip() expected = r"ic| test1: 'A\\veryvery\\long\\path\\to\\no\\even\\longer\\HelloWorld _01_Heritisfinallythe file.file'" self.assertEqual(curr_res, expected) del curr_res, expected with disable_coloring(), capture_standard_streams() as (_, err): ic(test2) curr_res = err.getvalue().strip() # expected = r"ic| test2: 'A\\veryvery\\long\\path\\to\\no\\even\\longer\\HelloWorld _01_Heritisfinallythe file.file'" expected = r"ic| test2: 'A\\veryvery\\long\\path\\to\\no\\even\\longer\\HelloWorld _01_Heritisfinallythe file.file'" self.assertEqual(curr_res, expected) del curr_res, expected with disable_coloring(), capture_standard_streams() as (_, err): ic(test3) curr_res = err.getvalue().strip() expected = r"""ic| test3: '''line line'''""" self.assertEqual(curr_res, expected) del curr_res, expected def test_sympy_dict_keys_do_not_crash(self): """Regression: ic() must not raise when dict keys are SymPy symbols.""" try: import sympy as sp except Exception: self.skipTest("sympy not installed") x, y = sp.symbols("x y") d = {x: "hello", y: "world"} with disable_coloring(), capture_standard_streams() as (out, err): # If the bug regresses, this line raises TypeError. ic(d) s = err.getvalue().strip() # Basic sanity checks without assuming exact formatting or ordering. self.assertIn("ic|", s) self.assertIn("hello", s) self.assertIn("world", s) def test_sympy_solve_result_does_not_crash(self): """Regression: ic() must handle SymPy solve() outputs.""" try: import sympy as sp except Exception: self.skipTest("sympy not installed") x, y = sp.symbols("x y") res = sp.solve([x + 2, y - 2]) # list/dict of symbolic items with disable_coloring(), capture_standard_streams() as (out, err): ic(res) s = err.getvalue() self.assertIn("ic|", s) # Don’t assert exact text; just ensure something printed. self.assertTrue(len(s) > 0) ================================================ FILE: tests/test_install.py ================================================ # -*- coding: utf-8 -*- # # IceCream - Never use print() to debug again # # Ansgar Grunseid # grunseid.com # grunseid@gmail.com # # License: MIT # import unittest import icecream from tests.test_icecream import ( disable_coloring, capture_standard_streams, parse_output_into_pairs) from tests.install_test_import import runMe class TestIceCreamInstall(unittest.TestCase): def test_install(self): icecream.install() with disable_coloring(), capture_standard_streams() as (out, err): runMe() assert parse_output_into_pairs(out, err, 1)[0][0] == ('x', '3') icecream.uninstall() # Clean up builtins. def test_uninstall(self): try: icecream.uninstall() except AttributeError: # Already uninstalled. pass # NameError: global name 'ic' is not defined. with self.assertRaises(NameError): runMe() ================================================ FILE: tox.ini ================================================ [tox] envlist = py39, py310, py311, py312, py313, py314, pypy3 [testenv] description = run unittest commands = python -m unittest deps = sympy>=1.12 [testenv:mypy] basepython = python3.9 deps = mypy==1.7.1 types-pygments types-colorama commands = mypy icecream