diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3f095ca81..7e970fac7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,11 +21,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v3 - - name: Set up Python 3.11 for linting - uses: actions/setup-python@v4.6.1 + uses: actions/checkout@v4 + - name: Set up Python 3.12 for linting + uses: actions/setup-python@v4.7.1 with: - python-version: '3.11' + python-version: '3.12' - name: Install dependencies run: |- python -m pip install --upgrade pip @@ -48,11 +48,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v4.6.1 + uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v4.7.1 with: - python-version: '3.11' + python-version: '3.12' - name: Upgrade pip run: |- python -m pip install --upgrade pip @@ -114,26 +114,27 @@ jobs: os: - ubuntu-latest python-version: - - '3.11' + - '3.12' arch: - auto steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 if: runner.os == 'Linux' && matrix.arch != 'auto' with: platforms: all - name: Setup Python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.1 with: python-version: ${{ matrix.python-version }} - name: Build pure wheel shell: bash run: |- - python -m pip install setuptools>=0.8 wheel build + python -m pip install setuptools>=0.8 wheel build twine python -m build --wheel --outdir wheelhouse + python -m twine check ./wheelhouse/ubelt*.whl - name: Show built files shell: bash run: ls -la wheelhouse @@ -164,23 +165,23 @@ jobs: install-extras: tests-strict,runtime-strict os: windows-latest arch: auto - - python-version: '3.11' + - python-version: '3.12' install-extras: tests-strict,runtime-strict,optional-strict os: ubuntu-latest arch: auto - - python-version: '3.11' + - python-version: '3.12' install-extras: tests-strict,runtime-strict,optional-strict os: macOS-latest arch: auto - - python-version: '3.11' + - python-version: '3.12' install-extras: tests-strict,runtime-strict,optional-strict os: windows-latest arch: auto - - python-version: '3.11' + - python-version: '3.12' install-extras: tests os: windows-latest arch: auto - - python-version: '3.11' + - python-version: '3.12' install-extras: tests os: windows-latest arch: auto @@ -208,6 +209,10 @@ jobs: install-extras: tests,optional os: windows-latest arch: auto + - python-version: '3.12' + install-extras: tests,optional + os: windows-latest + arch: auto - python-version: pypy-3.7 install-extras: tests,optional os: windows-latest @@ -236,6 +241,10 @@ jobs: install-extras: tests,optional os: windows-latest arch: auto + - python-version: '3.12' + install-extras: tests,optional + os: windows-latest + arch: auto - python-version: pypy-3.7 install-extras: tests,optional os: windows-latest @@ -264,6 +273,10 @@ jobs: install-extras: tests,optional os: windows-latest arch: auto + - python-version: '3.12' + install-extras: tests,optional + os: windows-latest + arch: auto - python-version: pypy-3.7 install-extras: tests,optional os: windows-latest @@ -282,17 +295,17 @@ jobs: arch: auto steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Enable MSVC 64bit uses: ilammy/msvc-dev-cmd@v1 if: matrix.os == 'windows-latest' - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 if: runner.os == 'Linux' && matrix.arch != 'auto' with: platforms: all - name: Setup Python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.1 with: python-version: ${{ matrix.python-version }} - uses: actions/download-artifact@v3 @@ -373,7 +386,7 @@ jobs: - test_purepy_wheels steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions/download-artifact@v3 name: Download wheels and sdist with: @@ -417,7 +430,7 @@ jobs: - test_purepy_wheels steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions/download-artifact@v3 name: Download wheels and sdist with: diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..d3bc49803 --- /dev/null +++ b/.mailmap @@ -0,0 +1,4 @@ +Jon Crall Jon Crall +Jon Crall jon.crall +Jon Crall joncrall +Jon Crall joncrall diff --git a/.readthedocs.yml b/.readthedocs.yml index c91d3a5c9..7a78a6c25 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,11 +7,14 @@ # Required version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.11" sphinx: configuration: docs/source/conf.py formats: all python: - version: 3.7 install: - requirements: requirements/docs.txt - method: pip diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ec47239..c25c66626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,20 @@ We are currently working on porting this changelog to the specifications in [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project (loosely) adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Version 1.3.3 - +## Version 1.3.4 - + +### Added +* Add backend option to `highlight_code` which can be "pygments" or "rich". +* Support for Python 3.12 + +### Changed +* Improve speed of inplace dictionary set operations. + +### Fixed +* Align in the case of `nobraces=1` for `ubelt.urepr`. + + +## Version 1.3.3 - 2023-07-10 ### Fixed @@ -12,6 +25,9 @@ This project (loosely) adheres to [Semantic Versioning](https://semver.org/spec/ ndarray with object type. * Actually exposed `ChDir`. +### Changed +* Docs and typing improvements + ### Notes * Skipped a release version due to a bad github tag. diff --git a/README.rst b/README.rst index 652a892ba..3e3e1e4cb 100644 --- a/README.rst +++ b/README.rst @@ -1044,6 +1044,25 @@ Libraries that contain one specific data structure or utility: * timerit: snippet timing for benchmarks - https://github.com/Erotemic/timerit +Jaraco (i.e. Jason R. Coombs) has an extensive library of utilities: + +* jaraco.classes - https://github.com/jaraco/jaraco.classes +* jaraco.collections - https://github.com/jaraco/jaraco.collections +* jaraco.context - https://github.com/jaraco/jaraco.context +* jaraco.crypto - https://github.com/jaraco/jaraco.crypto +* jaraco.functools - https://github.com/jaraco/jaraco.functools +* jaraco.geo - https://github.com/jaraco/jaraco.geo +* jaraco.imaging - https://github.com/jaraco/jaraco.imaging +* jaraco.itertools - https://github.com/jaraco/jaraco.itertools +* jaraco.logging - https://github.com/jaraco/jaraco.logging +* jaraco.media - https://github.com/jaraco/jaraco.media +* jaraco.path - https://github.com/jaraco/jaraco.path +* jaraco.text - https://github.com/jaraco/jaraco.text +* jaraco.util - https://github.com/jaraco/jaraco.util +* jaraco.windows - https://github.com/jaraco/jaraco.windows +* and many others not listed here. See: https://github.com/jaraco?tab=repositories&q=jaraco. + + Ubelt is included in the the [bestof-python list](https://github.com/ml-tooling/best-of-python), which contains many other tools that you should check out. diff --git a/dev/bench/bench_highlight.py b/dev/bench/bench_highlight.py new file mode 100644 index 000000000..74505cf78 --- /dev/null +++ b/dev/bench/bench_highlight.py @@ -0,0 +1,97 @@ +""" +Test if pygments or rich is faster when it comes to highlighting. + +Results: + pygments is a lot faster +""" +import sys +import ubelt as ub +import warnings + + +def _pygments_highlight(text, lexer_name, **kwargs): + """ + Original pygments highlight logic + """ + if sys.platform.startswith('win32'): # nocover + # Hack on win32 to support colored output + try: + import colorama + if not colorama.initialise.atexit_done: + # Only init if it hasn't been done + colorama.init() + except ImportError: + warnings.warn( + 'colorama is not installed, ansi colors may not work') + + import pygments # type: ignore + import pygments.lexers # type: ignore + import pygments.formatters # type: ignore + import pygments.formatters.terminal # type: ignore + + formatter = pygments.formatters.terminal.TerminalFormatter(bg='dark') + lexer = pygments.lexers.get_lexer_by_name(lexer_name, **kwargs) + new_text = pygments.highlight(text, lexer, formatter) + return new_text + + +def _rich_highlight(text, lexer_name): + """ + Alternative rich-based highlighter + + References: + https://github.com/Textualize/rich/discussions/3076 + """ + from rich.syntax import Syntax + from rich.console import Console + import io + syntax = Syntax(text, lexer_name, background_color='default') + stream = io.StringIO() + write_console = Console(file=stream, soft_wrap=True, color_system='standard') + write_console.print(syntax) + new_text = write_console.file.getvalue() + return new_text + + +def main(): + # Benchmark which is faster + import timerit + + lexer_name = 'python' + ti = timerit.Timerit(100, bestof=10, verbose=2) + + text = 'import ubelt as ub; print(ub)' + for timer in ti.reset('small-pygments'): + pygments_text = _pygments_highlight(text, lexer_name) + + for timer in ti.reset('small-rich'): + rich_text = _rich_highlight(text, lexer_name) + + print(pygments_text) + print(rich_text) + + # Use bigger text + try: + text = ub.Path(__file__).read_text() + except NameError: + text = ub.Path('~/code/ubelt/dev/bench/bench_highlight.py').expand().read_text() + + for timer in ti.reset('big-pygments'): + pygments_text = _pygments_highlight(text, lexer_name) + + for timer in ti.reset('big-rich'): + rich_text = _rich_highlight(text, lexer_name) + + print(pygments_text) + print(rich_text) + + print(ub.urepr(ti.measures['mean'], align=':', precision=8)) + print(ub.urepr(ti.measures['min'], align=':', precision=8)) + + +if __name__ == '__main__': + """ + CommandLine: + python ~/code/ubelt/dev/bench/bench_highlight.py + """ + main() diff --git a/dev/ci_public_gpg_key.pgp.enc b/dev/ci_public_gpg_key.pgp.enc index 18e52dd54..e349e6d5d 100644 --- a/dev/ci_public_gpg_key.pgp.enc +++ b/dev/ci_public_gpg_key.pgp.enc @@ -1,49 +1,49 @@ -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 -5dc5yjQXnapCLngJh1NZKQ== +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 +yieT/fIfAend3h/itWAZfw== diff --git a/dev/ci_secret_gpg_subkeys.pgp.enc b/dev/ci_secret_gpg_subkeys.pgp.enc index a9311f485..a3e707082 100644 --- a/dev/ci_secret_gpg_subkeys.pgp.enc +++ b/dev/ci_secret_gpg_subkeys.pgp.enc @@ -1,34 +1,34 @@ -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 diff --git a/dev/gpg_owner_trust.enc b/dev/gpg_owner_trust.enc index 739cc11f3..9984d0999 100644 --- a/dev/gpg_owner_trust.enc +++ b/dev/gpg_owner_trust.enc @@ -1,12 +1,12 @@ -U2FsdGVkX1+QGpOJCGC8HAVehtumD2kh47doDCDwnefW/VnI7th4ml2YL4rFP+BQ -pJ2eegxGgpTaz4sYRf7kos8kNyRajFJ9v0inE9eLM0g2rWxHV7H5e8cC+w6Af/Ib -1d1GgvgCzZHX3RBgl6BXKWZAJGqng+YDrzQziMTnjlvHhW/6teUXK7/fl4QXgy7j -oai7BkUIdExKDyTDTpgdr/EOBs6v4c/89Y1dc3z/WJ5DsYvI/5XFDwuPljNghvxI -L7m1xWWFcu/re9xg0m2nAiELVXO6wY8I7LOIpoSIsG7xC1FFwBD1/aw/LK/1wlrc -akAmBrwUh6otNqwabyJBiXXQ56EW48eiyK3PnpKzG0PvEIjMXOH01nrJF7a5emLw -oBzAQ7CA0RqJ0pfWGK0e/uekN/CpRl0bvZywcGGW6VMU1WJxXNwA0VYyfq8C5npb -dCFFeiMCL/5NSqoJxqVbCKMEbqKRpBa3uWVXoO2foRUdRbUz5icD/TTdvB2ff4+V -Rw5kg2+xz27soL5lRFClnR1CsLv+p1V9j8dvJ0cp6LZJmtcJGjsEuJNiKAxZUstU -K/oBM3t9du97iLZ/7C4ADMAskgdCEaNM+fIxvdyo5rtdCfyA7IyLB95uqMdFTcbH -WdZc6WXttUYhoMIxq30NwIV2wqoflavKOV4XDPWsHVWi4gHsJoBVc786ATPclqqa -xvbL2qTiA/SH4TW0EROKXg== +U2FsdGVkX18RHUIrYS0t6opiG6Nap3eXttjo3cMYFx7mhomYs3FvWOiwSG9iN514 +6a3Xr9rEcHatgOTbVrTHex57cv8hqvK+k2kxQFCeQxibHnyZWWekwG7xfX8cJYSp +88yeu9O8Zm9heqZ1QUNy1SDKIf0x2osQ5Mo1jWa3AbaOeck47AQT+MDMgFVAYaTt +jDDExn9Vf6IMDIdAe4/uviRGLCk7a7Rcgy1LlYQ2mNbqrj1w+tCSkIGoAF2Ig02f +Xc1cNoj+Tq6mx0TvB10S3WbjMQgqMqKugK20pALwe13Clfax2DVcef/V61V130BT +IqRmAOlVFJIQkIko19Zo4n/B/nqv92MjplanG8XQTadYOCaUKFtYb8gw6MBAdfAm +YFpdfSvogbOAA4glyz4UoDAVynvDw7lGSdDFpoinJFx3RSisGjkgRFuhY1ZAdTO7 +JWU/E0+XyNU6Mwfw4BlYlVEWroMJTHLZCI7ZPFlsx+wRxG6n6s5UPYae06G8jhD9 +hX7mIvU5i1jmTf7JboIXuRca0foOeFKcNe4K5agDT6mg3Kqp5QtK6KleZ0kLkC4B +Y8Vf6Yiys07eeFs6vCJXK7lx/xh2g/xHSvJ5cvkZ5/JwWIwOhl3horD2kVF7+N/3 +/TmA6YwMiSkV42lZ2esIWut/jEyfUYG1e4pqt9sa6QUh9CC7/zWcgiz+xj6vpP36 +agTE4D8GND1QYVgb4zUz5w== diff --git a/docs/source/conf.py b/docs/source/conf.py index 012317874..9caea3d2c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -45,11 +45,13 @@ Set the Repository NAME: ubelt Set the Repository URL: https://github.com/Erotemic/ubelt - For gitlab you also need to setup an integrations and add gitlab - incoming webhook + For gitlab you also need to setup an integrations. Navigate to: https://readthedocs.org/dashboard/ubelt/integrations/create/ + Then add gitlab incoming webhook and copy the URL (make sure + you copy the real url and not the text so https is included). + Then go to https://github.com/Erotemic/ubelt/hooks @@ -112,7 +114,7 @@ def visit_Assign(self, node): author = 'Jon Crall' modname = 'ubelt' -modpath = join(dirname(dirname(dirname(__file__))), modname, '__init__.py') +modpath = join(dirname(dirname(dirname(__file__))), 'ubelt', '__init__.py') release = parse_version(modpath) version = '.'.join(release.split('.')[0:2]) @@ -127,6 +129,7 @@ def visit_Assign(self, node): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + # 'autoapi.extension', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', @@ -134,6 +137,10 @@ def visit_Assign(self, node): 'sphinx.ext.todo', 'sphinx.ext.viewcode', # 'myst_parser', # TODO + + 'sphinx.ext.githubpages', + # 'sphinxcontrib.redirects', + 'sphinx_reredirects', ] todo_include_todos = True @@ -147,6 +154,15 @@ def visit_Assign(self, node): autoclass_content = 'both' # autodoc_mock_imports = ['torch', 'torchvision', 'visdom'] +# autoapi_modules = { +# modname: { +# 'override': False, +# 'output': 'auto' +# } +# } +# autoapi_dirs = [f'../../src/{modname}'] +# autoapi_keep_files = True + intersphinx_mapping = { # 'pytorch': ('http://pytorch.org/docs/master/', None), 'python': ('https://docs.python.org/3', None), @@ -163,7 +179,14 @@ def visit_Assign(self, node): 'xdoctest': ('https://xdoctest.readthedocs.io/en/latest/', None), 'networkx': ('https://networkx.org/documentation/stable/', None), 'scriptconfig': ('https://scriptconfig.readthedocs.io/en/latest/', None), - + 'rich': ('https://rich.readthedocs.io/en/latest/', None), + + 'pytest': ('https://docs.pytest.org/en/latest/', None), + # 'pytest._pytest.doctest': ('https://docs.pytest.org/en/latest/_modules/_pytest/doctest.html', None), + # 'colorama': ('https://pypi.org/project/colorama/', None), + # 'numpy': ('http://docs.scipy.org/doc/numpy/', None), + # 'cv2' : ('http://docs.opencv.org/2.4/', None), + # 'h5py' : ('http://docs.h5py.org/en/latest/', None) } __dev_note__ = """ python -m sphinx.ext.intersphinx https://docs.python.org/3/objects.inv @@ -173,6 +196,11 @@ def visit_Assign(self, node): python -m sphinx.ext.intersphinx https://kwimage.readthedocs.io/en/latest/objects.inv python -m sphinx.ext.intersphinx https://ubelt.readthedocs.io/en/latest/objects.inv python -m sphinx.ext.intersphinx https://networkx.org/documentation/stable/objects.inv + +sphobjinv suggest -t 90 -u https://readthedocs.org/projects/pytest/reference/objects.inv +"signal.convolve2d" + +python -m sphinx.ext.intersphinx https://pygments-doc.readthedocs.io/en/latest/objects.inv """ @@ -242,7 +270,7 @@ def visit_Assign(self, node): # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'ubeltdoc' +htmlhelp_basename = project + 'doc' # -- Options for LaTeX output ------------------------------------------------ @@ -309,8 +337,10 @@ class PatchedPythonDomain(PythonDomain): """ def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): # TODO: can use this to resolve references nicely - # if target.startswith('ub.'): - # target = 'ubelt.' + target[3] + if target.startswith('ub.'): + target = 'ubelt.' + target[3] + if target.startswith('xdoc.'): + target = 'xdoctest.' + target[3] return_value = super(PatchedPythonDomain, self).resolve_xref( env, fromdocname, builder, typ, target, node, contnode) return return_value @@ -517,7 +547,7 @@ def process_docstring_callback(self, app, what_: str, name: str, obj: Any, # import xdev # xdev.embed() - RENDER_IMAGES = 1 + RENDER_IMAGES = 0 if RENDER_IMAGES: # DEVELOPING if any('REQUIRES(--show)' in line for line in lines): diff --git a/pyproject.toml b/pyproject.toml index 777bed088..f1aa39f04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,13 +13,26 @@ rel_mod_parent_dpath = "." os = ["all"] min_python = 3.6 author = "Jon Crall" +typed = "partial" author_email = "erotemic@gmail.com" description = "A Python utility belt containing simple tools, a stdlib like feel, and extra batteries" license = "Apache 2" dev_status = "stable" +classifiers = [ + # List of classifiers available at: + # https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Utilities', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS', + 'Operating System :: POSIX :: Linux', + 'Typing :: Stubs Only', +] [tool.xcookie.setuptools] -keywords = ["utility", "python", "hashing", "caching", "stdlib", "path", "pathlib"] +keywords = ["utility", "python", "hashing", "caching", "stdlib", "path", "pathlib", "dictionary", "download"] [tool.pytest.ini_options] addopts = "-p no:doctest --xdoctest --xdoctest-style=google --ignore-glob=setup.py --ignore-glob=docs" diff --git a/requirements/optional.txt b/requirements/optional.txt index 3451c1ac6..8fa8f33eb 100644 --- a/requirements/optional.txt +++ b/requirements/optional.txt @@ -4,14 +4,16 @@ # xdev availpkg numpy # 1.19.2 was important for some versions of tensorflow -numpy>=1.23.5 ; python_version < '4.0' and python_version >= '3.11' and platform_python_implementation == "CPython" # Python 3.11+ +numpy>=1.26.0 ; python_version < '4.0' and python_version >= '3.12' and platform_python_implementation == "CPython" # Python 3.12+ +numpy>=1.23.2 ; python_version < '3.12' and python_version >= '3.11' and platform_python_implementation == "CPython" # Python 3.11 numpy>=1.21.1 ; python_version < '3.11' and python_version >= '3.10' and platform_python_implementation == "CPython" # Python 3.10 numpy>=1.19.3 ; python_version < '3.10' and python_version >= '3.9' and platform_python_implementation == "CPython" # Python 3.9 numpy>=1.19.2 ; python_version < '3.9' and python_version >= '3.8' and platform_python_implementation == "CPython" # Python 3.8 -numpy>=1.14.5 ; python_version < '3.8' and python_version >= '3.7' and platform_python_implementation == "CPython" # Python 3.7 -numpy>=1.12.0 ; python_version < '3.7' and python_version >= '3.6' and platform_python_implementation == "CPython" # Python 3.6 +numpy>=1.14.5,<2.0.0 ; python_version < '3.8' and python_version >= '3.7' and platform_python_implementation == "CPython" # Python 3.7 +numpy>=1.12.0,<2.0.0 ; python_version < '3.7' and python_version >= '3.6' and platform_python_implementation == "CPython" # Python 3.6 -xxhash>=3.2.0 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ +xxhash>=3.4.1 ; python_version < '4.0' and python_version >= '3.12' # Python 3.12+ +xxhash>=3.2.0 ; python_version < '3.12' and python_version >= '3.11' # Python 3.11 xxhash>=3.0.0 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 xxhash>=2.0.2 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 xxhash>=1.4.3 ; python_version < '3.9' and python_version >= '3.8' # Python 3.8 diff --git a/requirements/tests.txt b/requirements/tests.txt index 13e21ad3c..c06fd05fa 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,10 +1,9 @@ -xdoctest>=0.14.0 +xdoctest >= 1.1.2 # Pin maximum pytest versions for older python versions # TODO: determine what the actual minimum and maximum acceptable versions of # pytest (that are also compatible with xdoctest) are for each legacy python # major.minor version. -# See ~/local/tools/supported_python_versions_pip.py for helper script pytest>=6.2.5 ; python_version >= '3.10.0' # Python 3.10+ pytest>=4.6.0 ; python_version < '3.10.0' and python_version >= '3.7.0' # Python 3.7-3.9 pytest>=4.6.0 ; python_version < '3.7.0' and python_version >= '3.6.0' # Python 3.6 @@ -22,12 +21,12 @@ pytest-cov>=2.8.1 ; python_version < '3.5.0' and python_version >= '3 pytest-cov>=2.8.1 ; python_version < '2.8.0' and python_version >= '2.7.0' # Python 2.7 # xdev availpkg pytest-timeout -# xdev availpkg codecov pytest-timeout>=1.4.2 # xdev availpkg xdoctest # xdev availpkg coverage -coverage>=6.1.1 ; python_version >= '3.10' # Python 3.10+ +coverage>=7.3.0 ; python_version < '4.0' and python_version >= '3.12' # Python 3.12 +coverage>=6.1.1 ; python_version < '3.12' and python_version >= '3.10' # Python 3.10-3.11 coverage>=5.3.1 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 coverage>=6.1.1 ; python_version < '3.9' and python_version >= '3.8' # Python 3.8 coverage>=6.1.1 ; python_version < '3.8' and python_version >= '3.7' # Python 3.7 diff --git a/setup.py b/setup.py index 09ba4c282..07498bc8d 100755 --- a/setup.py +++ b/setup.py @@ -1,16 +1,9 @@ #!/usr/bin/env python -""" -Installation: - pip install git+https://github.com/Erotemic/ubelt.git - -Developing: - git clone https://github.com/Erotemic/ubelt.git - cd ubelt - pip install -e ubelt[all] -""" +# Generated by ~/code/xcookie/xcookie/builders/setup.py +# based on part ~/code/xcookie/xcookie/rc/setup.py.in import sys -from os.path import exists - +import re +from os.path import exists, dirname, join from setuptools import find_packages from setuptools import setup @@ -19,26 +12,38 @@ def parse_version(fpath): """ Statically parse the version number from a python file """ + value = static_parse("__version__", fpath) + return value + + +def static_parse(varname, fpath): + """ + Statically parse the a constant variable from a python file + """ import ast + if not exists(fpath): - raise ValueError('fpath={!r} does not exist'.format(fpath)) - with open(fpath, 'r') as file_: + raise ValueError("fpath={!r} does not exist".format(fpath)) + with open(fpath, "r") as file_: sourcecode = file_.read() pt = ast.parse(sourcecode) - class Finished(Exception): - pass - class VersionVisitor(ast.NodeVisitor): + + class StaticVisitor(ast.NodeVisitor): def visit_Assign(self, node): for target in node.targets: - if getattr(target, 'id', None) == '__version__': - self.version = node.value.s - raise Finished - visitor = VersionVisitor() + if getattr(target, "id", None) == varname: + self.static_value = node.value.s + + visitor = StaticVisitor() + visitor.visit(pt) try: - visitor.visit(pt) - except Finished: - pass - return visitor.version + value = visitor.static_value + except AttributeError: + import warnings + + value = "Unknown {}".format(varname) + warnings.warn(value) + return value def parse_description(): @@ -49,17 +54,16 @@ def parse_description(): pandoc --from=markdown --to=rst --output=README.rst README.md python -c "import setup; print(setup.parse_description())" """ - from os.path import dirname, join, exists - readme_fpath = join(dirname(__file__), 'README.rst') + readme_fpath = join(dirname(__file__), "README.rst") # This breaks on pip install, so check that it exists. if exists(readme_fpath): - with open(readme_fpath, 'r') as f: + with open(readme_fpath, "r") as f: text = f.read() return text - return '' + return "" -def parse_requirements(fname='requirements.txt', versions=False): +def parse_requirements(fname="requirements.txt", versions=False): """ Parse the package dependencies listed in a requirements file but strips specific versioning information. @@ -72,12 +76,13 @@ def parse_requirements(fname='requirements.txt', versions=False): Returns: List[str]: list of requirements items + + CommandLine: + python -c "import setup, ubelt; print(ubelt.urepr(setup.parse_requirements()))" """ - from os.path import exists, dirname, join - import re require_fpath = fname - def parse_line(line, dpath=''): + def parse_line(line, dpath=""): """ Parse information from a line in a requirements text file @@ -85,136 +90,183 @@ def parse_line(line, dpath=''): line = '-e git+https://a.com/somedep@sometag#egg=SomeDep' """ # Remove inline comments - comment_pos = line.find(' #') + comment_pos = line.find(" #") if comment_pos > -1: line = line[:comment_pos] - if line.startswith('-r '): + if line.startswith("-r "): # Allow specifying requirements in other files - target = join(dpath, line.split(' ')[1]) + target = join(dpath, line.split(" ")[1]) for info in parse_require_file(target): yield info else: # See: https://www.python.org/dev/peps/pep-0508/ - info = {'line': line} - if line.startswith('-e '): - info['package'] = line.split('#egg=')[1] + info = {"line": line} + if line.startswith("-e "): + info["package"] = line.split("#egg=")[1] else: - if ';' in line: - pkgpart, platpart = line.split(';') + if "--find-links" in line: + # setuptools doesnt seem to handle find links + line = line.split("--find-links")[0] + if ";" in line: + pkgpart, platpart = line.split(";") # Handle platform specific dependencies # setuptools.readthedocs.io/en/latest/setuptools.html # #declaring-platform-specific-dependencies plat_deps = platpart.strip() - info['platform_deps'] = plat_deps + info["platform_deps"] = plat_deps else: pkgpart = line platpart = None # Remove versioning from the package - pat = '(' + '|'.join(['>=', '==', '>']) + ')' + pat = "(" + "|".join([">=", "==", ">"]) + ")" parts = re.split(pat, pkgpart, maxsplit=1) parts = [p.strip() for p in parts] - info['package'] = parts[0] + info["package"] = parts[0] if len(parts) > 1: op, rest = parts[1:] version = rest # NOQA - info['version'] = (op, version) + info["version"] = (op, version) yield info def parse_require_file(fpath): dpath = dirname(fpath) - with open(fpath, 'r') as f: + with open(fpath, "r") as f: for line in f.readlines(): line = line.strip() - if line and not line.startswith('#'): + if line and not line.startswith("#"): for info in parse_line(line, dpath=dpath): yield info def gen_packages_items(): if exists(require_fpath): for info in parse_require_file(require_fpath): - parts = [info['package']] - if versions and 'version' in info: - if versions == 'strict': + parts = [info["package"]] + if versions and "version" in info: + if versions == "strict": # In strict mode, we pin to the minimum version - if info['version']: + if info["version"]: # Only replace the first >= instance - verstr = ''.join(info['version']).replace('>=', '==', 1) + verstr = "".join(info["version"]).replace(">=", "==", 1) parts.append(verstr) else: - parts.extend(info['version']) - if not sys.version.startswith('3.4'): + parts.extend(info["version"]) + if not sys.version.startswith("3.4"): # apparently package_deps are broken in 3.4 - plat_deps = info.get('platform_deps') + plat_deps = info.get("platform_deps") if plat_deps is not None: - parts.append(';' + plat_deps) - item = ''.join(parts) + parts.append(";" + plat_deps) + item = "".join(parts) yield item packages = list(gen_packages_items()) return packages -NAME = 'ubelt' -VERSION = parse_version('ubelt/__init__.py') +# # Maybe use in the future? But has private deps +# def parse_requirements_alt(fpath='requirements.txt', versions='loose'): +# """ +# Args: +# versions (str): can be +# False or "free" - remove all constraints +# True or "loose" - use the greater or equal (>=) in the req file +# strict - replace all greater equal with equals +# """ +# # Note: different versions of pip might have different internals. +# # This may need to be fixed. +# from pip._internal.req import parse_requirements +# from pip._internal.network.session import PipSession +# requirements = [] +# for req in parse_requirements(fpath, session=PipSession()): +# if not versions or versions == 'free': +# req_name = req.requirement.split(' ')[0] +# requirements.append(req_name) +# elif versions == 'loose' or versions is True: +# requirements.append(req.requirement) +# elif versions == 'strict': +# part1, *rest = req.requirement.split(';') +# strict_req = ';'.join([part1.replace('>=', '==')] + rest) +# requirements.append(strict_req) +# else: +# raise KeyError(versions) +# requirements = [r.replace(' ', '') for r in requirements] +# return requirements + +NAME = "ubelt" +INIT_PATH = "ubelt/__init__.py" +VERSION = parse_version(INIT_PATH) -if __name__ == '__main__': +if __name__ == "__main__": setupkw = {} - setupkw['install_requires'] = parse_requirements('requirements/runtime.txt') - setupkw['extras_require'] = { - 'all': parse_requirements('requirements.txt'), - 'tests': parse_requirements('requirements/tests.txt'), - 'optional': parse_requirements('requirements/optional.txt'), - 'docs': parse_requirements('requirements/docs.txt'), - 'all-strict': parse_requirements('requirements.txt', versions='strict'), - 'runtime-strict': parse_requirements( - 'requirements/runtime.txt', versions='strict' + + setupkw["install_requires"] = parse_requirements( + "requirements/runtime.txt", versions="loose" + ) + setupkw["extras_require"] = { + "all": parse_requirements("requirements.txt", versions="loose"), + "tests": parse_requirements("requirements/tests.txt", versions="loose"), + "optional": parse_requirements("requirements/optional.txt", versions="loose"), + "all": parse_requirements("requirements.txt", versions="loose"), + "runtime": parse_requirements("requirements/runtime.txt", versions="loose"), + "tests": parse_requirements("requirements/tests.txt", versions="loose"), + "optional": parse_requirements("requirements/optional.txt", versions="loose"), + "docs": parse_requirements("requirements/docs.txt", versions="loose"), + "types": parse_requirements("requirements/types.txt", versions="loose"), + "all-strict": parse_requirements("requirements.txt", versions="strict"), + "runtime-strict": parse_requirements( + "requirements/runtime.txt", versions="strict" ), - 'tests-strict': parse_requirements('requirements/tests.txt', versions='strict'), - 'docs-strict': parse_requirements('requirements/docs.txt', versions='strict'), - 'optional-strict': parse_requirements( - 'requirements/optional.txt', versions='strict' + "tests-strict": parse_requirements("requirements/tests.txt", versions="strict"), + "optional-strict": parse_requirements( + "requirements/optional.txt", versions="strict" ), + "docs-strict": parse_requirements("requirements/docs.txt", versions="strict"), + "types-strict": parse_requirements("requirements/types.txt", versions="strict"), } - setup( - name=NAME, - version=VERSION, - author='Jon Crall', - description=('A Python utility belt containing simple tools, ' - 'a stdlib like feel, and extra batteries.'), - long_description=parse_description(), - long_description_content_type='text/x-rst', - package_data={ - 'ubelt': ['py.typed', '*.pyi'], - }, - author_email='erotemic@gmail.com', - url='https://github.com/Erotemic/ubelt', - license='Apache 2', - packages=find_packages('.'), - python_requires='>=3.6', - classifiers=[ - # List of classifiers available at: - # https://pypi.python.org/pypi?%3Aaction=list_classifiers - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: MacOS', - 'Operating System :: POSIX :: Linux', - 'Typing :: Stubs Only', - # This should be interpreted as Apache License v2.0 - 'License :: OSI Approved :: Apache Software License', - # Supported Python versions - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - ], - **setupkw, - ) + setupkw["name"] = NAME + setupkw["version"] = VERSION + setupkw["author"] = "Jon Crall" + setupkw["author_email"] = "erotemic@gmail.com" + setupkw["url"] = "https://github.com/Erotemic/ubelt" + setupkw[ + "description" + ] = "A Python utility belt containing simple tools, a stdlib like feel, and extra batteries" + setupkw["long_description"] = parse_description() + setupkw["long_description_content_type"] = "text/x-rst" + setupkw["license"] = "Apache 2" + setupkw["packages"] = find_packages(".") + setupkw["python_requires"] = ">=3.6" + setupkw["classifiers"] = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + "License :: OSI Approved :: Apache Software License", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Typing :: Stubs Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ] + setupkw["package_data"] = {"ubelt": ["py.typed", "*.pyi"]} + setupkw["keywords"] = [ + "utility", + "python", + "hashing", + "caching", + "stdlib", + "path", + "pathlib", + "dictionary", + "download", + ] + setup(**setupkw) diff --git a/tests/test_repr.py b/tests/test_repr.py index b0d150f8a..db59dd0ea 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -399,6 +399,30 @@ def test_autosort(): } ''') + +def test_align_with_nobrace(): + data = {'123': 123, '45': 45, '6': 6} + text = ub.urepr(data, align=':') + print(text) + assert text == ub.codeblock( + ''' + { + '123': 123, + '45' : 45, + '6' : 6, + } + ''') + + text = ub.urepr(data, align=':', nobr=1) + print(text) + assert text == ub.codeblock( + ''' + '123': 123, + '45' : 45, + '6' : 6, + ''') + + if __name__ == '__main__': """ CommandLine: diff --git a/ubelt/__init__.py b/ubelt/__init__.py index a1abdd352..53b21f33d 100644 --- a/ubelt/__init__.py +++ b/ubelt/__init__.py @@ -24,7 +24,7 @@ xdoctest ubelt """ -__version__ = '1.3.3' +__version__ = '1.3.4' # Deprecated functions from ubelt.util_platform import ( diff --git a/ubelt/util_cache.py b/ubelt/util_cache.py index 607f8305d..b973d475a 100644 --- a/ubelt/util_cache.py +++ b/ubelt/util_cache.py @@ -1252,9 +1252,10 @@ def _byte_str(num, unit='auto', precision=2): str: string representing the number of bytes with appropriate units Example: + >>> from ubelt.util_cache import _byte_str >>> import ubelt as ub >>> num_list = [1, 100, 1024, 1048576, 1073741824, 1099511627776] - >>> result = ub.repr2(list(map(_byte_str, num_list)), nl=0) + >>> result = ub.urepr(list(map(_byte_str, num_list)), nl=0) >>> print(result) ['0.00KB', '0.10KB', '1.00KB', '1.00MB', '1.00GB', '1.00TB'] >>> _byte_str(10, unit='B') diff --git a/ubelt/util_colors.py b/ubelt/util_colors.py index 7750c1cf9..6aa2b21c1 100644 --- a/ubelt/util_colors.py +++ b/ubelt/util_colors.py @@ -21,6 +21,8 @@ value of the ``bool(os.environ.get('NO_COLOR'))`` flag, which is compliant with [NoColor]_. +New in 1.3.4: The :py:mod:`rich` backend was added as an alternative to pygments. + Related work: https://github.com/Textualize/rich @@ -42,25 +44,44 @@ NO_COLOR = bool(os.environ.get('NO_COLOR')) # type: bool -def highlight_code(text, lexer_name='python', **kwargs): +def highlight_code(text, lexer_name='python', backend='pygments', **kwargs): """ Highlights a block of text using ANSI tags based on language syntax. Args: - text (str): plain text to highlight - lexer_name (str): name of language. eg: python, docker, c++ - **kwargs: passed to pygments.lexers.get_lexer_by_name + text (str): + Plain text to parse and highlight + + lexer_name (str): + Name of language. eg: python, docker, c++. + For an exhaustive list see :func:`pygments.lexers.get_all_lexers`. + Defaults to "python". + + backend (str): + Either "pygments" or "rich". Defaults to "pygments". + + **kwargs: + If the backend is "pygments", passed to + pygments.lexers.get_lexer_by_name. Returns: str: - text - highlighted text If pygments is not installed, the plain - text is returned. + text - highlighted text if the requested backend is installed, + otherwise the plain text is returned unmodified. Example: >>> import ubelt as ub >>> text = 'import ubelt as ub; print(ub)' >>> new_text = ub.highlight_code(text) >>> print(new_text) + + Example: + >>> import ubelt as ub + >>> text = 'import ubelt as ub; print(ub)' + >>> new_text = ub.highlight_code(text, backend='pygments') + >>> print(new_text) + >>> new_text = ub.highlight_code(text, backend='rich') + >>> print(new_text) """ if NO_COLOR: return text @@ -73,30 +94,59 @@ def highlight_code(text, lexer_name='python', **kwargs): 'c': 'cpp', }.get(lexer_name.replace('.', ''), lexer_name) try: + if backend == 'pygments': + new_text = _pygments_highlight(text, lexer_name, **kwargs) + elif backend == 'rich': + new_text = _rich_highlight(text, lexer_name, **kwargs) + else: + raise KeyError(backend) + except ImportError: # nocover + warnings.warn(f'{backend} is not installed, code will not be highlighted') + new_text = text + return new_text - if sys.platform.startswith('win32'): # nocover - # Hack on win32 to support colored output - try: - import colorama - if not colorama.initialise.atexit_done: - # Only init if it hasn't been done - colorama.init() - except ImportError: - warnings.warn( - 'colorama is not installed, ansi colors may not work') - import pygments # type: ignore - import pygments.lexers # type: ignore - import pygments.formatters # type: ignore - import pygments.formatters.terminal # type: ignore +def _pygments_highlight(text, lexer_name, **kwargs): + """ + Original pygments highlight logic + """ + if sys.platform.startswith('win32'): # nocover + # Hack on win32 to support colored output + try: + import colorama + if not colorama.initialise.atexit_done: + # Only init if it hasn't been done + colorama.init() + except ImportError: + warnings.warn( + 'colorama is not installed, ansi colors may not work') + + import pygments # type: ignore + import pygments.lexers # type: ignore + import pygments.formatters # type: ignore + import pygments.formatters.terminal # type: ignore + + formatter = pygments.formatters.terminal.TerminalFormatter(bg='dark') + lexer = pygments.lexers.get_lexer_by_name(lexer_name, **kwargs) + new_text = pygments.highlight(text, lexer, formatter) + return new_text - formatter = pygments.formatters.terminal.TerminalFormatter(bg='dark') - lexer = pygments.lexers.get_lexer_by_name(lexer_name, **kwargs) - new_text = pygments.highlight(text, lexer, formatter) - except ImportError: # nocover - warnings.warn('pygments is not installed, code will not be highlighted') - new_text = text +def _rich_highlight(text, lexer_name): # nocover + """ + Alternative rich-based highlighter + + References: + https://github.com/Textualize/rich/discussions/3076 + """ + from rich.syntax import Syntax + from rich.console import Console + import io + syntax = Syntax(text, lexer_name, background_color='default') + stream = io.StringIO() + write_console = Console(file=stream, soft_wrap=True, color_system='standard') + write_console.print(syntax) + new_text = write_console.file.getvalue() return new_text @@ -116,6 +166,9 @@ def color_text(text, color): text - colorized text. If pygments is not installed plain text is returned. + SeeAlso: + https://rich.readthedocs.io/en/stable/markup.html + Example: >>> text = 'raw text' >>> import pytest @@ -137,6 +190,7 @@ def color_text(text, color): >>> # xdoctest: +REQUIRES(module:pygments) >>> import pygments.console >>> import ubelt as ub + >>> # List available colors codes >>> known_colors = pygments.console.codes.keys() >>> for color in known_colors: ... print(ub.color_text(color, color)) diff --git a/ubelt/util_colors.pyi b/ubelt/util_colors.pyi index 63372f725..a614a1c2a 100644 --- a/ubelt/util_colors.pyi +++ b/ubelt/util_colors.pyi @@ -1,7 +1,10 @@ NO_COLOR: bool -def highlight_code(text: str, lexer_name: str = 'python', **kwargs) -> str: +def highlight_code(text: str, + lexer_name: str = 'python', + backend: str = 'pygments', + **kwargs) -> str: ... diff --git a/ubelt/util_deprecate.py b/ubelt/util_deprecate.py index 20e6fdb67..748df322c 100644 --- a/ubelt/util_deprecate.py +++ b/ubelt/util_deprecate.py @@ -7,6 +7,9 @@ def schedule_deprecation(modname, name='?', type='?', migration='', deprecate=None, error=None, remove=None, + # TODO: let the user have more control over the + # message. + # message=None, warncls=DeprecationWarning, stacklevel=1): """ Deprecation machinery to help provide users with a smoother transition. diff --git a/ubelt/util_dict.py b/ubelt/util_dict.py index e93044cac..79ec326c5 100644 --- a/ubelt/util_dict.py +++ b/ubelt/util_dict.py @@ -406,10 +406,11 @@ def dict_union(*args): >>> import ubelt as ub >>> result = ub.dict_union({'a': 1, 'b': 1}, {'b': 2, 'c': 2}) >>> assert result == {'a': 1, 'b': 2, 'c': 2} - >>> ub.dict_union( + >>> output = ub.dict_union( >>> ub.odict([('a', 1), ('b', 2)]), >>> ub.odict([('c', 3), ('d', 4)])) - OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)]) + >>> print(ub.urepr(output, nl=0)) + {'a': 1, 'b': 2, 'c': 3, 'd': 4} >>> ub.dict_union() {} """ @@ -444,8 +445,9 @@ def dict_diff(*args): >>> import ubelt as ub >>> ub.dict_diff({'a': 1, 'b': 1}, {'a'}, {'c'}) {'b': 1} - >>> ub.dict_diff(odict([('a', 1), ('b', 2)]), odict([('c', 3)])) - OrderedDict([('a', 1), ('b', 2)]) + >>> result = ub.dict_diff(ub.odict([('a', 1), ('b', 2)]), ub.odict([('c', 3)])) + >>> print(ub.urepr(result, nl=0)) + {'a': 1, 'b': 2} >>> ub.dict_diff() {} >>> ub.dict_diff({'a': 1, 'b': 2}, {'c'}) @@ -1342,6 +1344,8 @@ def __rxor__(self, other): def __ior__(self, other): """ + The inplace union operator ``|=``. + Example: >>> import ubelt as ub >>> self = orig_ref = ub.sdict({1: 1, 2: 2, 3: 3}) @@ -1358,6 +1362,8 @@ def __ior__(self, other): def __iand__(self, other): """ + The inplace intersection operator ``&=``. + Example: >>> import ubelt as ub >>> self = orig_ref = ub.sdict({1: 1, 2: 2, 3: 3}) @@ -1369,13 +1375,15 @@ def __iand__(self, other): >>> assert self == (orig_val & other) self={1: 1, 2: 2} """ - result = self.intersection(other) - self.clear() - self.update(result) + remove_keys = self.keys() - set(other) + for k in remove_keys: + del self[k] return self def __isub__(self, other): """ + The inplace difference operator ``-=``. + Example: >>> import ubelt as ub >>> self = orig_ref = ub.sdict({1: 1, 2: 2, 3: 3}) @@ -1406,16 +1414,15 @@ def __isub__(self, other): >>> assert self is orig_ref >>> assert self == (orig_val - other) """ - result = self.difference(other) - self.clear() - self.update(result) - # common = UDict.intersection(self, other) - # for k in common: - # self.pop(k, None) + remove_keys = self.keys() & set(other) + for k in remove_keys: + del self[k] return self def __ixor__(self, other): """ + The inplace symmetric difference operator ``^=``. + Example: >>> import ubelt as ub >>> self = orig_ref = ub.sdict({1: 1, 2: 2, 3: 3}) @@ -1426,9 +1433,13 @@ def __ixor__(self, other): >>> assert self is orig_ref >>> assert self == (orig_val ^ other) """ - result = self.symmetric_difference(other) - self.clear() - self.update(result) + other_keys = set(other.keys()) + remove_keys = self.keys() & other_keys + add_keys = other_keys - remove_keys + for k in remove_keys: + del self[k] + for k in add_keys: + self[k] = other[k] return self ### Main set operations @@ -1477,7 +1488,12 @@ def union(self, *others, cls=None, merge=None): >>> print(ub.repr2(res, sort=1, nl=0, si=1)) {0: B_a, 2: C_c, 3: C_d, 4: B_e, 5: A_f, 7: B_h, 8: C_i, 9: D_j, 10: D_k, 11: D_l} """ - cls = cls or self.__class__ + if cls is None: + # Some subclass-constructors need special handling + # Not sure if it is in-scope to do that here or not. + # if isinstance(self.__class__, defaultdict): + # ... + cls = self.__class__ args = it.chain([self], others) if merge is None: new = cls(it.chain.from_iterable(d.items() for d in args)) diff --git a/ubelt/util_func.py b/ubelt/util_func.py index 5ca9ce5a1..92252297d 100644 --- a/ubelt/util_func.py +++ b/ubelt/util_func.py @@ -183,7 +183,17 @@ def func(a, e, /, f): 'a': 2, 'b': 3, 'c': 7, 'd': 11, 'e': 13, 'f': 17, } - func(1, 2, **ub.compatible(config, func)) + funckw = ub.compatible(config, func) + func(1, 2, **funckw) + + + ### While the stdlib inspect.signature is useful, it does not + ### have a concise way of getting the subset of the dictionary + ### that can be passed as keyword arguments. + import inspect + sig = inspect.signature(func) + funckw2 = ub.udict(config) & sig.parameters + ub.udict(report_config) & (sig.parameters) """ import inspect sig = inspect.signature(func) diff --git a/ubelt/util_futures.py b/ubelt/util_futures.py index c5163c724..31afb566b 100644 --- a/ubelt/util_futures.py +++ b/ubelt/util_futures.py @@ -269,6 +269,43 @@ class Executor(object): * :class:`SerialExecutor` * :class:`JobPool` + In the case where you cant or dont want to use ubelt.Executor you can get + similar behavior with the following pure-python snippet: + + .. code:: python + + def Executor(max_workers): + # Stdlib-only "ubelt.Executor"-like behavior + if max_workers == 1: + import contextlib + def submit_partial(func, *args, **kwargs): + def wrapper(): + return func(*args, **kwargs) + wrapper.result = wrapper + return wrapper + executor = contextlib.nullcontext() + executor.submit = submit_partial + else: + from concurrent.futures import ThreadPoolExecutor + executor = ThreadPoolExecutor(max_workers=max_workers) + return executor + + executor = Executor(0) + with executor: + jobs = [] + + for arg in range(1000): + job = executor.submit(chr, arg) + jobs.append(job) + + results = [] + for job in jobs: + result = job.result() + results.append(result) + + print('results = {}'.format(ub.urepr(results, nl=1))) + + Attributes: backend (SerialExecutor | ThreadPoolExecutor | ProcessPoolExecutor): diff --git a/ubelt/util_import.py b/ubelt/util_import.py index 5670e0ae6..ad0a8f95c 100644 --- a/ubelt/util_import.py +++ b/ubelt/util_import.py @@ -26,6 +26,8 @@ 'import_module_from_path', ] +IS_PY_GE_308 = sys.version_info[0] >= 3 and sys.version_info[1] >= 8 + class PythonPathContext(object): """ @@ -990,8 +992,8 @@ def _parse_static_node_value(node): # TODO: ast.Constant for 3.8 if isinstance(node, ast.Num): value = node.n - elif isinstance(node, ast.Str): - value = node.s + elif (isinstance(node, ast.Constant) and isinstance(node.value, str) if IS_PY_GE_308 else isinstance(node, ast.Str)): + value = node.value if IS_PY_GE_308 else node.s elif isinstance(node, ast.List): value = list(map(_parse_static_node_value, node.elts)) elif isinstance(node, ast.Tuple): diff --git a/ubelt/util_path.py b/ubelt/util_path.py index f778473a8..356251e99 100644 --- a/ubelt/util_path.py +++ b/ubelt/util_path.py @@ -167,7 +167,7 @@ def userhome(username=None): Args: username (str | None): name of a user on the system. If unspecified, the current user is - inferred. + inferred from standard environment variables. Returns: str: path to the specified home directory @@ -236,8 +236,8 @@ def shrinkuser(path, home='~'): Args: path (str | PathLike): path in system file structure home (str): symbol used to replace the home path. - Defaults to '~', but you might want to use '$HOME' or - '%USERPROFILE%' instead. + Defaults to ``'~'``, but you might want to use ``'$HOME'`` or + ``'%USERPROFILE%'`` instead. Returns: str: shortened path replacing the home directory with a symbol @@ -289,8 +289,13 @@ def ensuredir(dpath, mode=0o1777, verbose=0, recreate=False): default Args: - dpath (str | PathLike | Tuple[str | PathLike]): dir to ensure. - mode (int): octal mode of directory + dpath (str | PathLike | Tuple[str | PathLike]): + directory to create if it does not exist. + + mode (int): + octal permissions if a new directory is created. + Defaults to 0o1777. + verbose (int): verbosity recreate (bool): if True removes the directory and @@ -340,11 +345,18 @@ class ChDir: Context manager that changes the current working directory and then returns you to where you were. + This is nearly the same as the stdlib :func:`contextlib.chdir`, with the + exception that it will do nothing if the input path is None (i.e. the user + did not want to change directories). + Args: dpath (str | PathLike | None): The new directory to work in. If None, then the context manager is disabled. + SeeAlso: + :func:`contextlib.chdir` + Example: >>> import ubelt as ub >>> dpath = ub.Path.appdir('ubelt/tests/chdir').ensuredir() @@ -407,7 +419,7 @@ class TempDir: """ Context for creating and cleaning up temporary directories. - DEPRECATED. Use `tempfile` instead. + DEPRECATED. Use :mod:`tempfile` instead. Note: This exists because :class:`tempfile.TemporaryDirectory` was @@ -555,7 +567,6 @@ class Path(_PathBase): * :py:data:`pathlib.PurePath.anchor` * :py:data:`pathlib.PurePath.name` * :py:data:`pathlib.PurePath.parts` - * :py:data:`pathlib.PurePath.parts` * :py:data:`pathlib.PurePath.parent` * :py:data:`pathlib.PurePath.parents` * :py:data:`pathlib.PurePath.suffix` @@ -897,20 +908,24 @@ def ensuredir(self, mode=0o777): """ Concise alias of ``self.mkdir(parents=True, exist_ok=True)`` + Args: + mode (int): + octal permissions if a new directory is created. + Defaults to 0o777. + Returns: Path: returns itself Example: >>> import ubelt as ub >>> cache_dpath = ub.Path.appdir('ubelt').ensuredir() - >>> dpath = ub.Path(join(cache_dpath, 'ensuredir')) - >>> if dpath.exists(): - ... os.rmdir(dpath) + >>> dpath = ub.Path(cache_dpath, 'newdir') + >>> dpath.delete() >>> assert not dpath.exists() >>> dpath.ensuredir() >>> assert dpath.exists() >>> dpath.rmdir() - """ + """ self.mkdir(mode=mode, parents=True, exist_ok=True) return self @@ -919,7 +934,8 @@ def mkdir(self, mode=511, parents=False, exist_ok=False): Create a new directory at this given path. Note: - The ubelt variant is the same, except it returns the path as well. + The ubelt extension is the same as the original pathlib method, + except this returns returns the path instead of None. Args: mode (int) : permission bits diff --git a/ubelt/util_path.pyi b/ubelt/util_path.pyi index 388a67f21..36f08b10f 100644 --- a/ubelt/util_path.pyi +++ b/ubelt/util_path.pyi @@ -104,7 +104,7 @@ class Path: def delete(self) -> 'Path': ... - def ensuredir(self, mode: int = ...) -> 'Path': + def ensuredir(self, mode: int = 511) -> 'Path': ... def mkdir(self, diff --git a/ubelt/util_repr.py b/ubelt/util_repr.py index 2ffb73122..c5845a3f9 100644 --- a/ubelt/util_repr.py +++ b/ubelt/util_repr.py @@ -812,6 +812,8 @@ def _join_itemstrs(itemstrs, itemsep, newlines, _leaf_info, nobraces, if use_newline: sep = ',\n' if nobraces: + if align: + itemstrs = _align_lines(itemstrs, character=align) body_str = sep.join(itemstrs) if trailing_sep and len(itemstrs) > 0: body_str += ',' diff --git a/ubelt/util_stream.py b/ubelt/util_stream.py index c1fef6b2d..ae86d61e4 100644 --- a/ubelt/util_stream.py +++ b/ubelt/util_stream.py @@ -1,5 +1,6 @@ """ -Functions for capturing and redirecting IO streams. +Functions for capturing and redirecting IO streams with optional +tee-functionality. The :class:`CaptureStdout` captures all text sent to stdout and optionally prevents it from actually reaching stdout. @@ -27,8 +28,12 @@ class TeeStringIO(io.StringIO): Example: >>> import ubelt as ub + >>> import io >>> redirect = io.StringIO() >>> self = ub.TeeStringIO(redirect) + >>> self.write('spam') + >>> assert self.getvalue() == 'spam' + >>> assert redirect.getvalue() == 'spam' """ def __init__(self, redirect=None): """ @@ -57,6 +62,12 @@ def isatty(self): # nocover Note: Needed for ``IPython.embed`` to work properly when this class is used to override stdout / stderr. + + SeeAlso: + :meth:`io.IOBase.isatty` + + Returns: + bool """ return (self.redirect is not None and hasattr(self.redirect, 'isatty') and self.redirect.isatty()) @@ -66,14 +77,33 @@ def fileno(self): Returns underlying file descriptor of the redirected IOBase object if one exists. + Returns: + int : the integer corresponding to the file descriptor + + SeeAlso: + :meth:`io.IOBase.fileno` + Example: + >>> import ubelt as ub + >>> dpath = ub.Path.appdir('ubelt/tests/util_stream').ensuredir() + >>> fpath = dpath / 'fileno-test.txt' + >>> with open(fpath, 'w') as file: + >>> self = ub.TeeStringIO(file) + >>> descriptor = self.fileno() + >>> print(f'descriptor={descriptor}') + >>> assert isinstance(descriptor, int) + + Example: + >>> # Test errors >>> # Not sure the best way to test, this func is important for >>> # capturing stdout when ipython embedding + >>> import io >>> import pytest + >>> import ubelt as ub >>> with pytest.raises(io.UnsupportedOperation): - >>> TeeStringIO(redirect=io.StringIO()).fileno() + >>> ub.TeeStringIO(redirect=io.StringIO()).fileno() >>> with pytest.raises(io.UnsupportedOperation): - >>> TeeStringIO(None).fileno() + >>> ub.TeeStringIO(None).fileno() """ if self.redirect is not None: return self.redirect.fileno() @@ -85,6 +115,15 @@ def encoding(self): """ Gets the encoding of the `redirect` IO object + FIXME: + My complains that this violates the Liskov substitution principle + because the return type can be str or None, whereas the parent + class always returns a None. In the future we may raise an exception + instead of returning None. + + SeeAlso: + :py:obj:`io.TextIOBase.encoding` + Example: >>> import ubelt as ub >>> redirect = io.StringIO() @@ -94,14 +133,40 @@ def encoding(self): >>> redirect = io.TextIOWrapper(io.StringIO()) >>> assert ub.TeeStringIO(redirect).encoding is redirect.encoding """ + # mypy correctly complains if we include the return type, but we need + # to keep this buggy behavior for legacy reasons. + # Returns: + # None | str if self.redirect is not None: return self.redirect.encoding else: return super().encoding + @encoding.setter + def encoding(self, value): + # Adding a setter to make mypy happy + raise Exception('Cannot set encoding attribute') + def write(self, msg): """ Write to this and the redirected stream + + Args: + msg (str): the data to write + + SeeAlso: + :meth:`io.TextIOBase.write` + + Example: + >>> import ubelt as ub + >>> dpath = ub.Path.appdir('ubelt/tests/util_stream').ensuredir() + >>> fpath = dpath / 'write-test.txt' + >>> with open(fpath, 'w') as file: + >>> self = ub.TeeStringIO(file) + >>> n = self.write('hello world') + >>> assert n == 11 + >>> assert self.getvalue() == 'hello world' + >>> assert fpath.read_text() == 'hello world' """ if self.redirect is not None: self.redirect.write(msg) @@ -110,6 +175,9 @@ def write(self, msg): def flush(self): # nocover """ Flush to this and the redirected stream + + SeeAlso: + :meth:`io.IOBase.flush` """ if self.redirect is not None: self.redirect.flush() @@ -124,17 +192,24 @@ class CaptureStream(object): class CaptureStdout(CaptureStream): r""" - Context manager that captures stdout and stores it in an internal stream + Context manager that captures stdout and stores it in an internal stream. + + Depending on the value of ``supress``, the user can control if stdout is + printed (i.e. if stdout is tee-ed or supressed) while it is being captured. SeeAlso: - :func:`contextlib.redirect_stdout` + :func:`contextlib.redirect_stdout` - similar, but does not have the + ability to print stdout while it is being captured. Attributes: text (str | None): internal storage for the most recent part + parts (List[str]): internal storage for all parts + cap_stdout (None | TeeStringIO): internal stream proxy - orig_stdout (io.TextIOBase): internal pointer to the original stdout - stream + + orig_stdout (io.TextIOBase): + internal pointer to the original stdout stream Example: >>> import ubelt as ub @@ -164,8 +239,10 @@ class CaptureStdout(CaptureStream): def __init__(self, suppress=True, enabled=True): """ Args: - suppress (bool): if True, stdout is not printed while captured. + suppress (bool): + if True, stdout is not printed while captured. Defaults to True. + enabled (bool): does nothing if this is False. Defaults to True. """ diff --git a/ubelt/util_stream.pyi b/ubelt/util_stream.pyi index d502f3814..bd7a9670b 100644 --- a/ubelt/util_stream.pyi +++ b/ubelt/util_stream.pyi @@ -12,17 +12,21 @@ class TeeStringIO(io.StringIO): def __init__(self, redirect: io.IOBase | None = None) -> None: ... - def isatty(self): + def isatty(self) -> bool: ... - def fileno(self): + def fileno(self) -> int: ... @property def encoding(self): ... - def write(self, msg): + @encoding.setter + def encoding(self, value) -> None: + ... + + def write(self, msg: str): ... def flush(self): diff --git a/ubelt/util_time.py b/ubelt/util_time.py index 2066c4a80..3f249d545 100644 --- a/ubelt/util_time.py +++ b/ubelt/util_time.py @@ -9,12 +9,15 @@ The :class:`Timer` class is a context manager that times a block of indented code. It includes `tic` and `toc` methods a more matlab like feel. -Timerit is gone! Use the standalone and separate module :module:`timerit`. +Timerit is gone! Use the standalone and separate module :py:mod:`timerit`. See Also: + :mod:`tempora` - https://github.com/jaraco/tempora - time related utility functions from Jaraco + :mod:`pendulum` - https://github.com/sdispater/pendulum - drop in replacement for datetime + :mod:`arrow` - https://github.com/arrow-py/arrow """ import time @@ -221,8 +224,8 @@ def timeparse(stamp, default_timezone='local', allow_dateutil=True): Without any extra dependencies this will parse the output of :func:`ubelt.util_time.timestamp()` into a datetime object. In the case - where the format differs, `dateutil.parser.parse` will be used if the - `python-dateutil` package is installed. + where the format differs, :func:`dateutil.parser.parse` will be used if the + :py:mod:`python-dateutil` package is installed. Args: stamp (str):