diff -Nru python-utils-2.3.0/codecov.yml python-utils-3.3.3/codecov.yml --- python-utils-2.3.0/codecov.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/codecov.yml 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,2 @@ +codecov: + token: 046054bc-5013-4e26-b93e-f2720c0e7b84 diff -Nru python-utils-2.3.0/CONTRIBUTING.md python-utils-3.3.3/CONTRIBUTING.md --- python-utils-2.3.0/CONTRIBUTING.md 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/CONTRIBUTING.md 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,81 @@ +# Contributing to python-utils + +Bug reports, code and documentation contributions are welcome. You can help this +project also by using the development version and by reporting any bugs you might encounter + +## 1. Reporting bugs +It's important to provide following details when submitting a bug +- Python version +- python-utils version +- OS details + +If possible also provide a minimum reproducible working code. +## 2. Contributing Code and Docs + +Before working on a new feature or a bug, please browse [existing issues](https://github.com/WoLpH/python-utils/issues) +to see whether it has previously been discussed. + +If your change alters python-util's behaviour or interface, it's a good idea to +discuss it before you start working on it. + +If you are fixing an issue, the first step should be to create a test case that +reproduces the incorrect behaviour. That will also help you to build an +understanding of the issue at hand. + +Make sure to add relevant tests and update documentation in order to get +your PRs merged. We strictly adhere to 100% code coverage. + +### Development Environment + +#### Getting the code + +Go to and fork the project repository. + +```bash +# Clone your fork +$ git clone git@github.com:/python-utils.git + +# Enter the project directory +$ cd python-utils + +# Create a branch for your changes +$ git checkout -b my_awesome_branch +``` + +#### Testing +Before submitting any PR make sure your code passes all the tests. + +To run the full test-suite, make sure you have `tox` installed and run the following command: + +```bash +$ tox +``` + +Or to speed it up (replace 8 with your number of cores), run: + +```bash +$ tox -p8 +``` + +During development I recommend using pytest directly and installing the package in development mode. + +Create virtual environment and activate +```bash +$ python3 -m venv venv +$ source venv/bin/activate +``` +Install test requirements +```bash +$ cd python-utils +$ pip install -e ".[tests]" +``` +Run tests +```bash +$ py.test +``` + +Note that this won't run `flake8` yet, so once all the tests succeed you can run `flake8` to check for code style errors. + +```bash +$ flake8 +``` diff -Nru python-utils-2.3.0/.coveragerc python-utils-3.3.3/.coveragerc --- python-utils-2.3.0/.coveragerc 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/.coveragerc 2022-05-31 21:30:38.000000000 +0000 @@ -2,7 +2,7 @@ branch = True source = python_utils - tests + _python_utils_tests omit = */mock/* */nose/* @@ -10,6 +10,15 @@ source = python_utils [report] +fail_under = 100 exclude_lines = pragma: no cover @abc.abstractmethod + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + if typing.TYPE_CHECKING: diff -Nru python-utils-2.3.0/debian/changelog python-utils-3.3.3/debian/changelog --- python-utils-2.3.0/debian/changelog 2022-06-04 19:47:04.000000000 +0000 +++ python-utils-3.3.3/debian/changelog 2022-07-29 22:54:49.000000000 +0000 @@ -1,3 +1,19 @@ +python-utils (3.3.3-1) unstable; urgency=medium + + * New upstream release. + * Include documentation in package. + * Switch from dh --with python in debian/rules to a dh-sequence-python3. + * d/control: Build-Depends: Add to python3-pytest. + * d/control: Build-Depends: Drop python3-pytest-runner, unused. + * d/control: Update Standards-Version. + * d/control: Switch Architecture from any to all. + * d/control: Set Rules-Requires-Root to no. + * d/rules: Run test suite. + * d/rules: Remove python_utils.egg-info during clean target. + * d/tests: Run test suite via autopkgtest. + + -- Edward Betts Fri, 29 Jul 2022 23:54:49 +0100 + python-utils (2.3.0-3) unstable; urgency=medium [ Debian Janitor ] diff -Nru python-utils-2.3.0/debian/control python-utils-3.3.3/debian/control --- python-utils-2.3.0/debian/control 2022-06-04 19:47:04.000000000 +0000 +++ python-utils-3.3.3/debian/control 2022-07-29 22:54:49.000000000 +0000 @@ -2,23 +2,27 @@ Section: python Priority: optional Maintainer: Debian Python Team -Uploaders: Bernd Zeimetz -Build-Depends: debhelper-compat (= 13), dh-python, - python3-all-dev:any, +Uploaders: Bernd Zeimetz , Edward Betts +Build-Depends: debhelper-compat (= 13), + dh-sequence-python3, + dh-sequence-sphinxdoc, + python3-all, python3-setuptools, - python3-pytest, - python3-pytest-runner, -Standards-Version: 4.5.1 + python3-sphinx , + python3-pytest , + python3-loguru +Rules-Requires-Root: no +Standards-Version: 4.6.1 Homepage: https://github.com/WoLpH/python-utils Vcs-Git: https://salsa.debian.org/python-team/packages/python-utils.git Vcs-Browser: https://salsa.debian.org/python-team/packages/python-utils Package: python3-python-utils -Architecture: any +Architecture: all Depends: - ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, + ${sphinxdoc:Depends} Description: collection of small Python functions and classes Python Utils is a collection of small Python functions and classes which make common patterns shorter and easier. diff -Nru python-utils-2.3.0/debian/copyright python-utils-3.3.3/debian/copyright --- python-utils-2.3.0/debian/copyright 2022-06-04 19:47:04.000000000 +0000 +++ python-utils-3.3.3/debian/copyright 2022-07-29 22:54:38.000000000 +0000 @@ -71,6 +71,7 @@ Files: debian/* Copyright: 2017 Bernd Zeimetz + 2022 Edward Betts License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff -Nru python-utils-2.3.0/debian/doc-base python-utils-3.3.3/debian/doc-base --- python-utils-2.3.0/debian/doc-base 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/debian/doc-base 2022-07-29 22:46:08.000000000 +0000 @@ -0,0 +1,8 @@ +Document: python-utils +Title: python-utils Documentation +Author: Rick van Hattem +Section: Programming/Python + +Format: HTML +Index: /usr/share/doc/python3-python-utils/html/index.html +Files: /usr/share/doc/python3-python-utils/html/*.html diff -Nru python-utils-2.3.0/debian/patches/0001-avoid-documentation-privacy-breach.patch python-utils-3.3.3/debian/patches/0001-avoid-documentation-privacy-breach.patch --- python-utils-2.3.0/debian/patches/0001-avoid-documentation-privacy-breach.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/debian/patches/0001-avoid-documentation-privacy-breach.patch 2022-07-29 22:47:10.000000000 +0000 @@ -0,0 +1,43 @@ +From: Edward Betts +Date: Fri, 29 Jul 2022 22:56:45 +0100 +Subject: avoid documentation privacy breach + +--- + README.rst | 6 ------ + docs/index.rst | 6 ------ + 2 files changed, 12 deletions(-) + +diff --git a/README.rst b/README.rst +index 9924421..cf27db8 100644 +--- a/README.rst ++++ b/README.rst +@@ -1,12 +1,6 @@ + Useful Python Utils + ============================================================================== + +-.. image:: https://github.com/WoLpH/python-utils/actions/workflows/main.yml/badge.svg?branch=master +- :target: https://github.com/WoLpH/python-utils/actions/workflows/main.yml +- +-.. image:: https://coveralls.io/repos/WoLpH/python-utils/badge.svg?branch=master +- :target: https://coveralls.io/r/WoLpH/python-utils?branch=master +- + Python Utils is a collection of small Python functions and + classes which make common patterns shorter and easier. It is by no means a + complete collection but it has served me quite a bit in the past and I will +diff --git a/docs/index.rst b/docs/index.rst +index df4d31a..a72cecc 100644 +--- a/docs/index.rst ++++ b/docs/index.rst +@@ -1,12 +1,6 @@ + Welcome to Python Utils's documentation! + ======================================== + +-.. image:: https://github.com/WoLpH/python-utils/actions/workflows/main.yml/badge.svg?branch=master +- :target: https://github.com/WoLpH/python-utils/actions/workflows/main.yml/badge.svg?branch=master +- +-.. image:: https://coveralls.io/repos/WoLpH/python-utils/badge.svg?branch=master +- :target: https://coveralls.io/r/WoLpH/python-utils?branch=master +- + Contents: + + .. toctree:: diff -Nru python-utils-2.3.0/debian/patches/0002-drop-unwanted-pytest-options.patch python-utils-3.3.3/debian/patches/0002-drop-unwanted-pytest-options.patch --- python-utils-2.3.0/debian/patches/0002-drop-unwanted-pytest-options.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/debian/patches/0002-drop-unwanted-pytest-options.patch 2022-07-29 22:47:10.000000000 +0000 @@ -0,0 +1,22 @@ +From: Edward Betts +Date: Fri, 29 Jul 2022 23:47:01 +0100 +Subject: drop unwanted pytest options + +--- + pytest.ini | 3 --- + 1 file changed, 3 deletions(-) + +diff --git a/pytest.ini b/pytest.ini +index 5d49701..e28ed7d 100644 +--- a/pytest.ini ++++ b/pytest.ini +@@ -5,9 +5,6 @@ python_files = + + addopts = + --doctest-modules +- --cov python_utils +- --cov-report term-missing +- --mypy + + doctest_optionflags = + ALLOW_UNICODE diff -Nru python-utils-2.3.0/debian/patches/series python-utils-3.3.3/debian/patches/series --- python-utils-2.3.0/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/debian/patches/series 2022-07-29 22:47:10.000000000 +0000 @@ -0,0 +1,2 @@ +0001-avoid-documentation-privacy-breach.patch +0002-drop-unwanted-pytest-options.patch diff -Nru python-utils-2.3.0/debian/python3-python-utils.docs python-utils-3.3.3/debian/python3-python-utils.docs --- python-utils-2.3.0/debian/python3-python-utils.docs 2022-06-04 19:47:04.000000000 +0000 +++ python-utils-3.3.3/debian/python3-python-utils.docs 2022-07-29 22:46:08.000000000 +0000 @@ -1 +1,2 @@ README.rst +docs/_build/html diff -Nru python-utils-2.3.0/debian/rules python-utils-3.3.3/debian/rules --- python-utils-2.3.0/debian/rules 2022-06-04 19:47:04.000000000 +0000 +++ python-utils-3.3.3/debian/rules 2022-07-29 22:54:49.000000000 +0000 @@ -1,10 +1,17 @@ #!/usr/bin/make -f -export PYTEST_RUNNER=false +export PYBUILD_TEST_PYTEST=1 %: - dh $@ --with=python3 \ - --buildsystem=pybuild + dh $@ --buildsystem=pybuild -override_dh_auto_test: - @echo "Test harness not supported" +override_dh_auto_build: + dh_auto_build +ifeq (,$(findstring nodocs, $(DEB_BUILD_OPTIONS))) + cd docs && sphinx-build -M html . _build +endif + + +override_dh_auto_clean: + dh_auto_clean + rm -rf python_utils.egg-info docs/_build diff -Nru python-utils-2.3.0/debian/tests/control python-utils-3.3.3/debian/tests/control --- python-utils-2.3.0/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/debian/tests/control 2022-07-29 22:54:49.000000000 +0000 @@ -0,0 +1,2 @@ +Tests: run-tests +Depends: python3-all, python3-pytest, python3-loguru, @ diff -Nru python-utils-2.3.0/debian/tests/run-tests python-utils-3.3.3/debian/tests/run-tests --- python-utils-2.3.0/debian/tests/run-tests 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/debian/tests/run-tests 2022-07-29 22:54:49.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh +set -e +cp -r _python_utils_tests "$AUTOPKGTEST_TMP/" && cd "$AUTOPKGTEST_TMP" +for py in $(py3versions -s); do + $py -Wd -m pytest -v -x 2>&1 +done diff -Nru python-utils-2.3.0/docs/conf.py python-utils-3.3.3/docs/conf.py --- python-utils-2.3.0/docs/conf.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/conf.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,33 +1,37 @@ -# -*- coding: utf-8 -*- +# Configuration file for the Sphinx documentation builder. # -# Python Utils documentation build configuration file, created by -# sphinx-quickstart on Wed May 9 16:57:31 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html -import os -import sys -import datetime +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. +# +from datetime import date +import os +import sys sys.path.insert(0, os.path.abspath('..')) + from python_utils import __about__ -# -- General configuration ----------------------------------------------------- +# -- Project information ----------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +project = 'Python Utils' +author = __about__.__author__ +copyright = f'{date.today().year}, {author}' + +# The full version, including alpha/beta/rc tags +release = __about__.__version__ -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', @@ -40,281 +44,23 @@ # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Python Utils' -copyright = u'%s, %s' % ( - datetime.date.today().year, - __about__.__author__, -) - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = __about__.__version__ -# The full version, including alpha/beta/rc tags. -release = __about__.__version__ - -# Monkey patch to disable nonlocal image warning -import sphinx -if hasattr(sphinx, 'environment'): - original_warn_mode = sphinx.environment.BuildEnvironment.warn_node - - def allow_nonlocal_image_warn_node(self, msg, *args, **kwargs): - if not msg.startswith('nonlocal image URI found:'): - original_warn_mode(self, msg, *args, **kwargs) - - sphinx.environment.BuildEnvironment.warn_node = \ - allow_nonlocal_image_warn_node - -suppress_warnings = [ - 'image.nonlocal_uri', -] - -needs_sphinx = '1.4' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'wolph' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_theme'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None +# +html_theme = 'alabaster' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'PythonUtilsdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'PythonUtils.tex', u'Python Utils Documentation', - __about__.__author__, 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'pythonutils', u'Python Utils Documentation', - [__about__.__author__], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'PythonUtils', u'Python Utils Documentation', - __about__.__author__, 'PythonUtils', __about__.__description__, - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# -- Options for Epub output --------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'Python Utils' -epub_author = __about__.__author__ -epub_publisher = __about__.__author__ -epub_copyright = copyright - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -#epub_exclude_files = [] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True +# html_static_path = ['_static'] +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} diff -Nru python-utils-2.3.0/docs/index.rst python-utils-3.3.3/docs/index.rst --- python-utils-2.3.0/docs/index.rst 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/index.rst 2022-05-31 21:30:38.000000000 +0000 @@ -1,6 +1,12 @@ Welcome to Python Utils's documentation! ======================================== +.. image:: https://github.com/WoLpH/python-utils/actions/workflows/main.yml/badge.svg?branch=master + :target: https://github.com/WoLpH/python-utils/actions/workflows/main.yml/badge.svg?branch=master + +.. image:: https://coveralls.io/repos/WoLpH/python-utils/badge.svg?branch=master + :target: https://coveralls.io/r/WoLpH/python-utils?branch=master + Contents: .. toctree:: @@ -9,16 +15,6 @@ usage python_utils -Travis status: - -.. image:: https://travis-ci.org/WoLpH/python-utils.png?branch=master - :target: https://travis-ci.org/WoLpH/python-utils - -Coverage: - -.. image:: https://coveralls.io/repos/WoLpH/python-utils/badge.png?branch=master - :target: https://coveralls.io/r/WoLpH/python-utils?branch=master - Indices and tables ================== diff -Nru python-utils-2.3.0/docs/make.bat python-utils-3.3.3/docs/make.bat --- python-utils-2.3.0/docs/make.bat 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/make.bat 2022-05-31 21:30:38.000000000 +0000 @@ -1,190 +1,35 @@ @ECHO OFF +pushd %~dp0 + REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) +set SOURCEDIR=. set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) if "%1" == "" goto help -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. echo. - echo.Build finished; now you can process the JSON files. - goto end + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 ) -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonUtils.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonUtils.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end +popd diff -Nru python-utils-2.3.0/docs/Makefile python-utils-3.3.3/docs/Makefile --- python-utils-2.3.0/docs/Makefile 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/Makefile 2022-05-31 21:30:38.000000000 +0000 @@ -1,153 +1,20 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . BUILDDIR = _build -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonUtils.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonUtils.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonUtils" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonUtils" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." +.PHONY: help Makefile -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff -Nru python-utils-2.3.0/docs/python_utils.rst python-utils-3.3.3/docs/python_utils.rst --- python-utils-2.3.0/docs/python_utils.rst 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/python_utils.rst 2022-05-31 21:30:38.000000000 +0000 @@ -4,6 +4,14 @@ Submodules ---------- +python\_utils\.decorators module +-------------------------------- + +.. automodule:: python_utils.decorators + :members: + :undoc-members: + :show-inheritance: + python\_utils\.converters module -------------------------------- diff -Nru python-utils-2.3.0/docs/requirements.txt python-utils-3.3.3/docs/requirements.txt --- python-utils-2.3.0/docs/requirements.txt 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/requirements.txt 2022-05-31 21:30:38.000000000 +0000 @@ -1,3 +1 @@ -mock -sphinx -python-utils +-e .[docs] diff -Nru python-utils-2.3.0/docs/_theme/flask_theme_support.py python-utils-3.3.3/docs/_theme/flask_theme_support.py --- python-utils-2.3.0/docs/_theme/flask_theme_support.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/_theme/flask_theme_support.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff -Nru python-utils-2.3.0/docs/_theme/LICENSE python-utils-3.3.3/docs/_theme/LICENSE --- python-utils-2.3.0/docs/_theme/LICENSE 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/_theme/LICENSE 1970-01-01 00:00:00.000000000 +0000 @@ -1,46 +0,0 @@ -Modifications: - -Copyright (c) 2012 Rick van Hattem. - - -Original Projects: - -Copyright (c) 2010 Kenneth Reitz. -Copyright (c) 2010 by Armin Ronacher. - - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff -Nru python-utils-2.3.0/docs/_theme/wolph/layout.html python-utils-3.3.3/docs/_theme/wolph/layout.html --- python-utils-2.3.0/docs/_theme/wolph/layout.html 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/_theme/wolph/layout.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - - {% endif %} - -{% endblock %} -{%- block relbar2 %}{% endblock %} -{%- block footer %} - -{%- endblock %} diff -Nru python-utils-2.3.0/docs/_theme/wolph/relations.html python-utils-3.3.3/docs/_theme/wolph/relations.html --- python-utils-2.3.0/docs/_theme/wolph/relations.html 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/_theme/wolph/relations.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -

Related Topics

- diff -Nru python-utils-2.3.0/docs/_theme/wolph/static/flasky.css_t python-utils-3.3.3/docs/_theme/wolph/static/flasky.css_t --- python-utils-2.3.0/docs/_theme/wolph/static/flasky.css_t 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/_theme/wolph/static/flasky.css_t 1970-01-01 00:00:00.000000000 +0000 @@ -1,431 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro'; - font-size: 17px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 0px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #555; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input[type="text"] { - width: 160px!important; -} -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #004B6B; -} - -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} - - -/* scrollbars */ - -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-button:start:decrement, -::-webkit-scrollbar-button:end:increment { - display: block; - height: 10px; -} - -::-webkit-scrollbar-button:vertical:increment { - background-color: #fff; -} - -::-webkit-scrollbar-track-piece { - background-color: #eee; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:vertical { - height: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:horizontal { - width: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -/* misc. */ - -.revsys-inline { - display: none!important; -} diff -Nru python-utils-2.3.0/docs/_theme/wolph/static/small_flask.css python-utils-3.3.3/docs/_theme/wolph/static/small_flask.css --- python-utils-2.3.0/docs/_theme/wolph/static/small_flask.css 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/_theme/wolph/static/small_flask.css 1970-01-01 00:00:00.000000000 +0000 @@ -1,70 +0,0 @@ -/* - * small_flask.css_t - * ~~~~~~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -body { - margin: 0; - padding: 20px 30px; -} - -div.documentwrapper { - float: none; - background: white; -} - -div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; -} - -div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, -div.sphinxsidebar h3 a { - color: white; -} - -div.sphinxsidebar a { - color: #aaa; -} - -div.sphinxsidebar p.logo { - display: none; -} - -div.document { - width: 100%; - margin: 0; -} - -div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; -} - -div.related ul, -div.related ul li { - margin: 0; - padding: 0; -} - -div.footer { - display: none; -} - -div.bodywrapper { - margin: 0; -} - -div.body { - min-height: 0; - padding: 0; -} diff -Nru python-utils-2.3.0/docs/_theme/wolph/theme.conf python-utils-3.3.3/docs/_theme/wolph/theme.conf --- python-utils-2.3.0/docs/_theme/wolph/theme.conf 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/docs/_theme/wolph/theme.conf 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -touch_icon = diff -Nru python-utils-2.3.0/.github/workflows/main.yml python-utils-3.3.3/.github/workflows/main.yml --- python-utils-2.3.0/.github/workflows/main.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/.github/workflows/main.yml 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,50 @@ +name: pytest + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 4 + strategy: + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools flake8 + pip install -e '.[tests]' + - name: Get versions + run: | + python -V + pip freeze + - name: flake8 + run: flake8 -v python_utils setup.py + - name: pytest + run: py.test + + docs: + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + pip install -e '.[docs]' + - name: build docs + run: make html + working-directory: docs/ diff -Nru python-utils-2.3.0/MANIFEST.in python-utils-3.3.3/MANIFEST.in --- python-utils-2.3.0/MANIFEST.in 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/MANIFEST.in 2022-05-31 21:30:38.000000000 +0000 @@ -7,4 +7,5 @@ include setup.cfg include setup.py include tox.ini -recursive-include tests * +recursive-include _python_utils_tests *.py *.txt +recursive-exclude __pycache__ * diff -Nru python-utils-2.3.0/pytest.ini python-utils-3.3.3/pytest.ini --- python-utils-2.3.0/pytest.ini 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/pytest.ini 2022-05-31 21:30:38.000000000 +0000 @@ -1,23 +1,16 @@ [pytest] python_files = - python_Utils/*.py - tests/*.py + python_utils/*.py + _python_utils_tests/*.py addopts = + --doctest-modules --cov python_utils - --cov-report html --cov-report term-missing - --doctest-modules - --pep8 - --flakes - -pep8ignore = - *.py W391 - docs/*.py ALL + --mypy -flakes-ignore = - docs/*.py ALL - -doctest_optionflags = +doctest_optionflags = ALLOW_UNICODE ALLOW_BYTES + +asyncio_mode = strict diff -Nru python-utils-2.3.0/python_utils/__about__.py python-utils-3.3.3/python_utils/__about__.py --- python-utils-2.3.0/python_utils/__about__.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/__about__.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,9 +1,10 @@ -__package_name__ = 'python-utils' -__version__ = '2.2.0' -__author__ = 'Rick van Hattem' -__author_email__ = 'Wolph@wol.ph' -__description__ = ( +__package_name__: str = 'python-utils' +__author__: str = 'Rick van Hattem' +__author_email__: str = 'Wolph@wol.ph' +__description__: str = ( 'Python Utils is a module with some convenient utilities not included ' - 'with the standard Python install') -__url__ = 'https://github.com/WoLpH/python-utils' - + 'with the standard Python install' +) +__url__: str = 'https://github.com/WoLpH/python-utils' +# Omit type info due to automatic versioning script +__version__ = '3.3.3' diff -Nru python-utils-2.3.0/python_utils/aio.py python-utils-3.3.3/python_utils/aio.py --- python-utils-2.3.0/python_utils/aio.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/python_utils/aio.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,16 @@ +''' +Asyncio equivalents to regular Python functions. + +''' +import asyncio +import itertools + + +async def acount(start=0, step=1, delay=0, stop=None): + '''Asyncio version of itertools.count()''' + for item in itertools.count(start, step): # pragma: no branch + if stop is not None and item >= stop: + break + + yield item + await asyncio.sleep(delay) diff -Nru python-utils-2.3.0/python_utils/containers.py python-utils-3.3.3/python_utils/containers.py --- python-utils-2.3.0/python_utils/containers.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/python_utils/containers.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,179 @@ +from __future__ import annotations + +import abc +import typing +from typing import Any, Generator + +from . import types + +if typing.TYPE_CHECKING: + import _typeshed # noqa: F401 + +KT = types.TypeVar('KT') +VT = types.TypeVar('VT') +DT = types.Dict[KT, VT] +KT_cast = types.Optional[types.Callable[[Any], KT]] +VT_cast = types.Optional[types.Callable[[Any], VT]] + +# Using types.Union instead of | since Python 3.7 doesn't fully support it +DictUpdateArgs = types.Union[ + types.Mapping, + types.Iterable[types.Union[types.Tuple[Any, Any], types.Mapping]], + '_typeshed.SupportsKeysAndGetItem[KT, VT]', +] + + +class CastedDictBase(types.Dict[KT, VT], abc.ABC): + _key_cast: KT_cast + _value_cast: VT_cast + + def __init__( + self, + key_cast: KT_cast = None, + value_cast: VT_cast = None, + *args, + **kwargs, + ) -> None: + self._value_cast = value_cast + self._key_cast = key_cast + self.update(*args, **kwargs) + + def update(self, *args: DictUpdateArgs, **kwargs: VT) -> None: + if args: + kwargs.update(*args) + + if kwargs: + for key, value in kwargs.items(): + self[key] = value + + def __setitem__(self, key: Any, value: Any) -> None: + if self._key_cast is not None: + key = self._key_cast(key) + + return super().__setitem__(key, value) + + +class CastedDict(CastedDictBase): + ''' + Custom dictionary that casts keys and values to the specified typing. + + Note that you can specify the types for mypy and type hinting with: + CastedDict[int, int](int, int) + + >>> d = CastedDict(int, int) + >>> d[1] = 2 + >>> d['3'] = '4' + >>> d.update({'5': '6'}) + >>> d.update([('7', '8')]) + >>> d + {1: 2, 3: 4, 5: 6, 7: 8} + >>> list(d.keys()) + [1, 3, 5, 7] + >>> list(d) + [1, 3, 5, 7] + >>> list(d.values()) + [2, 4, 6, 8] + >>> list(d.items()) + [(1, 2), (3, 4), (5, 6), (7, 8)] + >>> d[3] + 4 + + # Casts are optional and can be disabled by passing None as the cast + >>> d = CastedDict() + >>> d[1] = 2 + >>> d['3'] = '4' + >>> d.update({'5': '6'}) + >>> d.update([('7', '8')]) + >>> d + {1: 2, '3': '4', '5': '6', '7': '8'} + ''' + + def __setitem__(self, key, value): + if self._value_cast is not None: + value = self._value_cast(value) + + super().__setitem__(key, value) + + +class LazyCastedDict(CastedDictBase): + ''' + Custom dictionary that casts keys and lazily casts values to the specified + typing. Note that the values are cast only when they are accessed and + are not cached between executions. + + Note that you can specify the types for mypy and type hinting with: + LazyCastedDict[int, int](int, int) + + >>> d = LazyCastedDict(int, int) + >>> d[1] = 2 + >>> d['3'] = '4' + >>> d.update({'5': '6'}) + >>> d.update([('7', '8')]) + >>> d + {1: 2, 3: '4', 5: '6', 7: '8'} + >>> list(d.keys()) + [1, 3, 5, 7] + >>> list(d) + [1, 3, 5, 7] + >>> list(d.values()) + [2, 4, 6, 8] + >>> list(d.items()) + [(1, 2), (3, 4), (5, 6), (7, 8)] + >>> d[3] + 4 + + # Casts are optional and can be disabled by passing None as the cast + >>> d = LazyCastedDict() + >>> d[1] = 2 + >>> d['3'] = '4' + >>> d.update({'5': '6'}) + >>> d.update([('7', '8')]) + >>> d + {1: 2, '3': '4', '5': '6', '7': '8'} + >>> list(d.keys()) + [1, '3', '5', '7'] + >>> list(d.values()) + [2, '4', '6', '8'] + + >>> list(d.items()) + [(1, 2), ('3', '4'), ('5', '6'), ('7', '8')] + >>> d['3'] + '4' + ''' + + def __setitem__(self, key, value): + if self._key_cast is not None: + key = self._key_cast(key) + + super().__setitem__(key, value) + + def __getitem__(self, key) -> VT: + if self._key_cast is not None: + key = self._key_cast(key) + + value = super().__getitem__(key) + + if self._value_cast is not None: + value = self._value_cast(value) + + return value + + def items(self) -> Generator[tuple[KT, VT], None, None]: # type: ignore + if self._value_cast is None: + yield from super().items() + else: + for key, value in super().items(): + yield key, self._value_cast(value) + + def values(self) -> Generator[VT, None, None]: # type: ignore + if self._value_cast is None: + yield from super().values() + else: + for value in super().values(): + yield self._value_cast(value) + + +if __name__ == '__main__': + import doctest + + doctest.testmod() diff -Nru python-utils-2.3.0/python_utils/converters.py python-utils-3.3.3/python_utils/converters.py --- python-utils-2.3.0/python_utils/converters.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/converters.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,13 +1,18 @@ -from __future__ import (absolute_import, division, print_function, - unicode_literals) - -import re -import six +import decimal import math +import re +import typing +from . import types -def to_int(input_, default=0, exception=(ValueError, TypeError), regexp=None): - ''' + +def to_int( + input_: typing.Optional[str] = None, + default: int = 0, + exception: types.ExceptionsType = (ValueError, TypeError), + regexp: types.O[types.Pattern] = None, +) -> int: + r''' Convert the given input to an integer or return default When trying to convert the exceptions given in the exception parameter @@ -25,6 +30,10 @@ 0 >>> to_int('1') 1 + >>> to_int('') + 0 + >>> to_int() + 0 >>> to_int('abc123') 0 >>> to_int('123abc') @@ -37,23 +46,23 @@ 123 >>> to_int('abc123abc456', regexp=True) 123 - >>> to_int('abc123', regexp=re.compile('(\d+)')) + >>> to_int('abc123', regexp=re.compile(r'(\d+)')) 123 - >>> to_int('123abc', regexp=re.compile('(\d+)')) + >>> to_int('123abc', regexp=re.compile(r'(\d+)')) 123 - >>> to_int('abc123abc', regexp=re.compile('(\d+)')) + >>> to_int('abc123abc', regexp=re.compile(r'(\d+)')) 123 - >>> to_int('abc123abc456', regexp=re.compile('(\d+)')) + >>> to_int('abc123abc456', regexp=re.compile(r'(\d+)')) 123 - >>> to_int('abc123', regexp='(\d+)') + >>> to_int('abc123', regexp=r'(\d+)') 123 - >>> to_int('123abc', regexp='(\d+)') + >>> to_int('123abc', regexp=r'(\d+)') 123 - >>> to_int('abc', regexp='(\d+)') + >>> to_int('abc', regexp=r'(\d+)') 0 - >>> to_int('abc123abc', regexp='(\d+)') + >>> to_int('abc123abc', regexp=r'(\d+)') 123 - >>> to_int('abc123abc456', regexp='(\d+)') + >>> to_int('abc123abc456', regexp=r'(\d+)') 123 >>> to_int('1234', default=1) 1234 @@ -64,10 +73,9 @@ ... TypeError: unknown argument for regexp parameter: 123 ''' - if regexp is True: - regexp = re.compile('(\d+)') - elif isinstance(regexp, six.string_types): + regexp = re.compile(r'(\d+)') + elif isinstance(regexp, str): regexp = re.compile(regexp) elif hasattr(regexp, 'search'): pass @@ -75,18 +83,26 @@ raise TypeError('unknown argument for regexp parameter: %r' % regexp) try: - if regexp: + if regexp and input_: match = regexp.search(input_) if match: input_ = match.groups()[-1] - return int(input_) - except exception: + + if input_ is None: + return default + else: + return int(input_) + except exception: # type: ignore return default -def to_float(input_, default=0, exception=(ValueError, TypeError), - regexp=None): - ''' +def to_float( + input_: str, + default: int = 0, + exception: types.ExceptionsType = (ValueError, TypeError), + regexp: types.O[types.Pattern] = None, +) -> types.Number: + r''' Convert the given `input_` to an integer or return default When trying to convert the exceptions given in the exception parameter @@ -110,23 +126,23 @@ '123.00' >>> '%.2f' % to_float('abc0.456', regexp=True) '0.46' - >>> '%.2f' % to_float('abc123.456', regexp=re.compile('(\d+\.\d+)')) + >>> '%.2f' % to_float('abc123.456', regexp=re.compile(r'(\d+\.\d+)')) '123.46' - >>> '%.2f' % to_float('123.456abc', regexp=re.compile('(\d+\.\d+)')) + >>> '%.2f' % to_float('123.456abc', regexp=re.compile(r'(\d+\.\d+)')) '123.46' - >>> '%.2f' % to_float('abc123.46abc', regexp=re.compile('(\d+\.\d+)')) + >>> '%.2f' % to_float('abc123.46abc', regexp=re.compile(r'(\d+\.\d+)')) '123.46' - >>> '%.2f' % to_float('abc123abc456', regexp=re.compile('(\d+(\.\d+|))')) + >>> '%.2f' % to_float('abc123abc456', regexp=re.compile(r'(\d+(\.\d+|))')) '123.00' - >>> '%.2f' % to_float('abc', regexp='(\d+)') + >>> '%.2f' % to_float('abc', regexp=r'(\d+)') '0.00' - >>> '%.2f' % to_float('abc123', regexp='(\d+)') + >>> '%.2f' % to_float('abc123', regexp=r'(\d+)') '123.00' - >>> '%.2f' % to_float('123abc', regexp='(\d+)') + >>> '%.2f' % to_float('123abc', regexp=r'(\d+)') '123.00' - >>> '%.2f' % to_float('abc123abc', regexp='(\d+)') + >>> '%.2f' % to_float('abc123abc', regexp=r'(\d+)') '123.00' - >>> '%.2f' % to_float('abc123abc456', regexp='(\d+)') + >>> '%.2f' % to_float('abc123abc456', regexp=r'(\d+)') '123.00' >>> '%.2f' % to_float('1234', default=1) '1234.00' @@ -139,8 +155,8 @@ ''' if regexp is True: - regexp = re.compile('(\d+(\.\d+|))') - elif isinstance(regexp, six.string_types): + regexp = re.compile(r'(\d+(\.\d+|))') + elif isinstance(regexp, str): regexp = re.compile(regexp) elif hasattr(regexp, 'search'): pass @@ -157,11 +173,15 @@ return default -def to_unicode(input_, encoding='utf-8', errors='replace'): +def to_unicode( + input_: types.StringTypes, + encoding: str = 'utf-8', + errors: str = 'replace', +) -> str: '''Convert objects to unicode, if needed decodes string with the given encoding and errors settings. - :rtype: unicode + :rtype: str >>> to_unicode(b'a') 'a' @@ -175,14 +195,18 @@ >>> to_unicode(Foo) "" ''' - if isinstance(input_, six.binary_type): + if isinstance(input_, bytes): input_ = input_.decode(encoding, errors) else: - input_ = six.text_type(input_) + input_ = str(input_) return input_ -def to_str(input_, encoding='utf-8', errors='replace'): +def to_str( + input_: types.StringTypes, + encoding: str = 'utf-8', + errors: str = 'replace', +) -> bytes: '''Convert objects to string, encodes to the given encoding :rtype: str @@ -199,17 +223,20 @@ >>> to_str(Foo) "" ''' - if isinstance(input_, six.binary_type): + if isinstance(input_, bytes): pass else: if not hasattr(input_, 'encode'): - input_ = six.text_type(input_) + input_ = str(input_) input_ = input_.encode(encoding, errors) return input_ -def scale_1024(x, n_prefixes): +def scale_1024( + x: types.Number, + n_prefixes: int, +) -> types.Tuple[types.Number, types.Number]: '''Scale a number down to a suitable size, based on powers of 1024. Returns the scaled number and the power of 1024 used. @@ -234,3 +261,133 @@ scaled = float(x) / (2 ** (10 * power)) return scaled, power + +def remap( + value: types.DecimalNumber, + old_min: types.DecimalNumber, + old_max: types.DecimalNumber, + new_min: types.DecimalNumber, + new_max: types.DecimalNumber, +) -> types.DecimalNumber: + ''' + remap a value from one range into another. + + >>> remap(500, 0, 1000, 0, 100) + 50 + >>> remap(250.0, 0.0, 1000.0, 0.0, 100.0) + 25.0 + >>> remap(-75, -100, 0, -1000, 0) + -750 + >>> remap(33, 0, 100, -500, 500) + -170 + >>> remap(decimal.Decimal('250.0'), 0.0, 1000.0, 0.0, 100.0) + Decimal('25.0') + + This is a great use case example. Take an AVR that has dB values the + minimum being -80dB and the maximum being 10dB and you want to convert + volume percent to the equilivint in that dB range + + >>> remap(46.0, 0.0, 100.0, -80.0, 10.0) + -38.6 + + I added using decimal.Decimal so floating point math errors can be avoided. + Here is an example of a floating point math error + >>> 0.1 + 0.1 + 0.1 + 0.30000000000000004 + + If floating point remaps need to be done my suggstion is to pass at least + one parameter as a `decimal.Decimal`. This will ensure that the output + from this function is accurate. I left passing `floats` for backwards + compatability and there is no conversion done from float to + `decimal.Decimal` unless one of the passed parameters has a type of + `decimal.Decimal`. This will ensure that any existing code that uses this + funtion will work exactly how it has in the past. + + Some edge cases to test + >>> remap(1, 0, 0, 1, 2) + Traceback (most recent call last): + ... + ValueError: Input range (0-0) is empty + + >>> remap(1, 1, 2, 0, 0) + Traceback (most recent call last): + ... + ValueError: Output range (0-0) is empty + + :param value: value to be converted + :type value: int, float, decimal.Decimal + + :param old_min: minimum of the range for the value that has been passed + :type old_min: int, float, decimal.Decimal + + :param old_max: maximum of the range for the value that has been passed + :type old_max: int, float, decimal.Decimal + + :param new_min: the minimum of the new range + :type new_min: int, float, decimal.Decimal + + :param new_max: the maximum of the new range + :type new_max: int, float, decimal.Decimal + + :return: value that has been re ranged. if any of the parameters passed is + a `decimal.Decimal` all of the parameters will be converted to + `decimal.Decimal`. The same thing also happens if one of the + parameters is a `float`. otherwise all parameters will get converted + into an `int`. technically you can pass a `str` of an integer and it + will get converted. The returned value type will be `decimal.Decimal` + of any of the passed parameters ar `decimal.Decimal`, the return type + will be `float` if any of the passed parameters are a `float` otherwise + the returned type will be `int`. + + :rtype: int, float, decimal.Decimal + ''' + type_: types.Type[types.DecimalNumber] + if ( + isinstance(value, decimal.Decimal) + or isinstance(old_min, decimal.Decimal) + or isinstance(old_max, decimal.Decimal) + or isinstance(new_min, decimal.Decimal) + or isinstance(new_max, decimal.Decimal) + ): + type_ = decimal.Decimal + elif ( + isinstance(value, float) + or isinstance(old_min, float) + or isinstance(old_max, float) + or isinstance(new_min, float) + or isinstance(new_max, float) + ): + type_ = float + + else: + type_ = int + + value = type_(value) + old_min = type_(old_min) + old_max = type_(old_max) + new_max = type_(new_max) + new_min = type_(new_min) + + old_range = old_max - old_min # type: ignore + new_range = new_max - new_min # type: ignore + + if old_range == 0: + raise ValueError( + 'Input range ({}-{}) is empty'.format(old_min, old_max) + ) + + if new_range == 0: + raise ValueError( + 'Output range ({}-{}) is empty'.format(new_min, new_max) + ) + + new_value = (value - old_min) * new_range # type: ignore + + if type_ == int: + new_value //= old_range # type: ignore + else: + new_value /= old_range # type: ignore + + new_value += new_min # type: ignore + + return new_value diff -Nru python-utils-2.3.0/python_utils/decorators.py python-utils-3.3.3/python_utils/decorators.py --- python-utils-2.3.0/python_utils/decorators.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/python_utils/decorators.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,130 @@ +import functools +import logging +import random +from . import types + + +def set_attributes(**kwargs): + '''Decorator to set attributes on functions and classes + + A common usage for this pattern is the Django Admin where + functions can get an optional short_description. To illustrate: + + Example from the Django admin using this decorator: + https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display + + Our simplified version: + + >>> @set_attributes(short_description='Name') + ... def upper_case_name(self, obj): + ... return ("%s %s" % (obj.first_name, obj.last_name)).upper() + + The standard Django version: + + >>> def upper_case_name(obj): + ... return ("%s %s" % (obj.first_name, obj.last_name)).upper() + + >>> upper_case_name.short_description = 'Name' + + ''' + + def _set_attributes(function): + for key, value in kwargs.items(): + setattr(function, key, value) + return function + + return _set_attributes + + +def listify(collection: types.Callable = list, allow_empty: bool = True): + ''' + Convert any generator to a list or other type of collection. + + >>> @listify() + ... def generator(): + ... yield 1 + ... yield 2 + ... yield 3 + + >>> generator() + [1, 2, 3] + + >>> @listify() + ... def empty_generator(): + ... pass + + >>> empty_generator() + [] + + >>> @listify(allow_empty=False) + ... def empty_generator_not_allowed(): + ... pass + + >>> empty_generator_not_allowed() + Traceback (most recent call last): + ... + TypeError: 'NoneType' object is not iterable + + >>> @listify(collection=set) + ... def set_generator(): + ... yield 1 + ... yield 1 + ... yield 2 + + >>> set_generator() + {1, 2} + + >>> @listify(collection=dict) + ... def dict_generator(): + ... yield 'a', 1 + ... yield 'b', 2 + + >>> dict_generator() + {'a': 1, 'b': 2} + ''' + + def _listify(function): + @functools.wraps(function) + def __listify(*args, **kwargs): + result = function(*args, **kwargs) + if result is None and allow_empty: + return [] + return collection(result) + + return __listify + + return _listify + + +def sample(sample_rate: float): + ''' + Limit calls to a function based on given sample rate. + Number of calls to the function will be roughly equal to + sample_rate percentage. + + Usage: + + >>> @sample(0.5) + ... def demo_function(*args, **kwargs): + ... return 1 + + Calls to *demo_function* will be limited to 50% approximatly. + + ''' + + def _sample(function): + @functools.wraps(function) + def __sample(*args, **kwargs): + if random.random() < sample_rate: + return function(*args, **kwargs) + else: + logging.debug( + 'Skipped execution of %r(%r, %r) due to sampling', + function, + args, + kwargs, + ) # noqa: E501 + + return __sample + + return _sample diff -Nru python-utils-2.3.0/python_utils/exceptions.py python-utils-3.3.3/python_utils/exceptions.py --- python-utils-2.3.0/python_utils/exceptions.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/python_utils/exceptions.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,26 @@ +import typing + + +def raise_exception( + exception_class: typing.Type[Exception], + *args: typing.Any, + **kwargs: typing.Any, +) -> typing.Callable: + ''' + Returns a function that raises an exception of the given type with the + given arguments. + + >>> raise_exception(ValueError, 'spam')('eggs') + Traceback (most recent call last): + ... + ValueError: spam + ''' + + def raise_(*args_: typing.Any, **kwargs_: typing.Any) -> typing.Any: + raise exception_class(*args, **kwargs) + + return raise_ + + +def reraise(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: + raise diff -Nru python-utils-2.3.0/python_utils/formatters.py python-utils-3.3.3/python_utils/formatters.py --- python-utils-2.3.0/python_utils/formatters.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/formatters.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,8 +1,10 @@ import datetime +from python_utils import types -def camel_to_underscore(name): - '''Convert camel case style naming to underscore style naming + +def camel_to_underscore(name: str) -> str: + '''Convert camel case style naming to underscore/snake case style naming If there are existing underscores they will be collapsed with the to-be-added underscores. Multiple consecutive capital letters will not be @@ -30,7 +32,7 @@ elif i > 3 and not c.isupper(): # Will return the last 3 letters to check if we are changing # case - previous = name[i - 3:i] + previous = name[i - 3 : i] if previous.isalpha() and previous.isupper(): output.insert(len(output) - 1, '_') @@ -39,7 +41,44 @@ return ''.join(output) -def timesince(dt, default='just now'): +def apply_recursive( + function: types.Callable[[str], str], + data: types.OptionalScope = None, + **kwargs +) -> types.OptionalScope: + ''' + Apply a function to all keys in a scope recursively + + >>> apply_recursive(camel_to_underscore, {'SpamEggsAndBacon': 'spam'}) + {'spam_eggs_and_bacon': 'spam'} + >>> apply_recursive(camel_to_underscore, {'SpamEggsAndBacon': { + ... 'SpamEggsAndBacon': 'spam', + ... }}) + {'spam_eggs_and_bacon': {'spam_eggs_and_bacon': 'spam'}} + + >>> a = {'a_b_c': 123, 'def': {'DeF': 456}} + >>> b = apply_recursive(camel_to_underscore, a) + >>> b + {'a_b_c': 123, 'def': {'de_f': 456}} + + >>> apply_recursive(camel_to_underscore, None) + ''' + if data is None: + return None + + elif isinstance(data, dict): + return { + function(key): apply_recursive(function, value, **kwargs) + for key, value in data.items() + } + else: + return data + + +def timesince( + dt: types.Union[datetime.datetime, datetime.timedelta], + default: str = 'just now', +) -> str: ''' Returns string representing 'time since' e.g. 3 days ago, 5 hours ago etc. @@ -110,4 +149,3 @@ return '%s ago' % ' and '.join(output[:2]) return default - diff -Nru python-utils-2.3.0/python_utils/generators.py python-utils-3.3.3/python_utils/generators.py --- python-utils-2.3.0/python_utils/generators.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/python_utils/generators.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,79 @@ +import asyncio +import time + +import python_utils +from python_utils import types + + +async def abatcher( + generator: types.AsyncIterator, + batch_size: types.Optional[int] = None, + interval: types.Optional[types.delta_type] = None, +): + ''' + Asyncio generator wrapper that returns items with a given batch size or + interval (whichever is reached first). + ''' + batch: list = [] + + assert batch_size or interval, 'Must specify either batch_size or interval' + + # If interval is specified, use it to determine when to yield the batch + # Alternatively set a really long timeout to keep the code simpler + if interval: + interval_s = python_utils.delta_to_seconds(interval) + else: + # Set the timeout to 10 years + interval_s = 60 * 60 * 24 * 365 * 10.0 + + next_yield = time.perf_counter() + interval_s + + pending: types.Set = set() + + while True: + try: + done, pending = await asyncio.wait( + pending + or [ + asyncio.create_task(generator.__anext__()), # type: ignore + ], + timeout=interval_s, + return_when=asyncio.FIRST_COMPLETED, + ) + + if done: + for result in done: + batch.append(result.result()) + + except StopAsyncIteration: + if batch: + yield batch + + break + + if batch_size is not None and len(batch) == batch_size: + yield batch + batch = [] + + if interval and batch and time.perf_counter() > next_yield: + yield batch + batch = [] + # Always set the next yield time to the current time. If the + # loop is running slow due to blocking functions we do not + # want to burst too much + next_yield = time.perf_counter() + interval_s + + +def batcher(iterable, batch_size): + ''' + Generator wrapper that returns items with a given batch size + ''' + batch = [] + for item in iterable: + batch.append(item) + if len(batch) == batch_size: + yield batch + batch = [] + + if batch: + yield batch diff -Nru python-utils-2.3.0/python_utils/import_.py python-utils-3.3.3/python_utils/import_.py --- python-utils-2.3.0/python_utils/import_.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/import_.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,11 +1,18 @@ +from . import types + class DummyException(Exception): pass def import_global( - name, modules=None, exceptions=DummyException, locals_=None, - globals_=None, level=-1): + name: str, + modules: types.Optional[types.List[str]] = None, + exceptions: types.ExceptionsType = DummyException, + locals_: types.OptionalScope = None, + globals_: types.OptionalScope = None, + level: int = -1, +) -> types.Any: '''Import the requested items into the global scope WARNING! this method _will_ overwrite your global scope @@ -22,11 +29,14 @@ relative imports ''' frame = None + name_parts: types.List[str] = name.split('.') + modules_set: types.Set[str] = set() try: # If locals_ or globals_ are not given, autodetect them by inspecting # the current stack if locals_ is None or globals_ is None: import inspect + frame = inspect.stack()[1][0] if locals_ is None: @@ -36,44 +46,50 @@ globals_ = frame.f_globals try: - name = name.split('.') - # Relative imports are supported (from .spam import eggs) - if not name[0]: - name = name[1:] + if not name_parts[0]: + name_parts = name_parts[1:] level = 1 # raise IOError((name, level)) module = __import__( - name=name[0] or '.', + name=name_parts[0] or '.', globals=globals_, locals=locals_, - fromlist=name[1:], + fromlist=name_parts[1:], level=max(level, 0), ) # Make sure we get the right part of a dotted import (i.e. # spam.eggs should return eggs, not spam) try: - for attr in name[1:]: + for attr in name_parts[1:]: module = getattr(module, attr) except AttributeError: - raise ImportError('No module named ' + '.'.join(name)) + raise ImportError('No module named ' + '.'.join(name_parts)) # If no list of modules is given, autodetect from either __all__ # or a dir() of the module if not modules: - modules = getattr(module, '__all__', dir(module)) + modules_set = set(getattr(module, '__all__', dir(module))) else: - modules = set(modules).intersection(dir(module)) + modules_set = set(modules).intersection(dir(module)) # Add all items in modules to the global scope - for k in set(dir(module)).intersection(modules): + for k in set(dir(module)).intersection(modules_set): if k and k[0] != '_': globals_[k] = getattr(module, k) except exceptions as e: return e finally: # Clean up, just to be sure - del name, modules, exceptions, locals_, globals_, frame - + del ( + name, + name_parts, + modules, + modules_set, + exceptions, + locals_, + globals_, + frame, + ) diff -Nru python-utils-2.3.0/python_utils/__init__.py python-utils-3.3.3/python_utils/__init__.py --- python-utils-2.3.0/python_utils/__init__.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/__init__.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,73 @@ +from . import ( + aio, + compat, + converters, + decorators, + formatters, + generators, + import_, + logger, + terminal, + time, + types, +) +from .aio import acount +from .converters import remap, scale_1024, to_float, to_int, to_str, to_unicode +from .decorators import listify, set_attributes +from .exceptions import raise_exception, reraise +from .formatters import camel_to_underscore, timesince +from .generators import abatcher, batcher +from .import_ import import_global +from .logger import Logged, LoggerBase +from .terminal import get_terminal_size +from .time import ( + aio_generator_timeout_detector, + aio_generator_timeout_detector_decorator, + aio_timeout_generator, + delta_to_seconds, + delta_to_seconds_or_none, + format_time, + timedelta_to_seconds, + timeout_generator, +) + +__all__ = [ + 'aio', + 'generators', + 'compat', + 'converters', + 'decorators', + 'formatters', + 'import_', + 'logger', + 'terminal', + 'time', + 'types', + 'to_int', + 'to_float', + 'to_unicode', + 'to_str', + 'scale_1024', + 'remap', + 'set_attributes', + 'listify', + 'camel_to_underscore', + 'timesince', + 'import_global', + 'get_terminal_size', + 'timedelta_to_seconds', + 'format_time', + 'timeout_generator', + 'acount', + 'abatcher', + 'batcher', + 'aio_timeout_generator', + 'aio_generator_timeout_detector_decorator', + 'aio_generator_timeout_detector', + 'delta_to_seconds', + 'delta_to_seconds_or_none', + 'reraise', + 'raise_exception', + 'Logged', + 'LoggerBase', +] diff -Nru python-utils-2.3.0/python_utils/logger.py python-utils-3.3.3/python_utils/logger.py --- python-utils-2.3.0/python_utils/logger.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/logger.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,18 +1,24 @@ -import logging +import abc import functools +import logging __all__ = ['Logged'] +import typing -class Logged(object): - '''Class which automatically adds a named logger to your class when - interiting + +class LoggerBase(abc.ABC): + '''Class which automatically adds logging utilities to your class when + interiting. Expects `logger` to be a logging.Logger or compatible instance. Adds easy access to debug, info, warning, error, exception and log methods - >>> class MyClass(Logged): + >>> class MyClass(LoggerBase): + ... logger = logging.getLogger(__name__) + ... ... def __init__(self): ... Logged.__init__(self) + >>> my_class = MyClass() >>> my_class.debug('debug') >>> my_class.info('info') @@ -21,42 +27,76 @@ >>> my_class.exception('exception') >>> my_class.log(0, 'log') ''' - def __new__(cls, *args, **kwargs): - cls.logger = logging.getLogger( - cls.__get_name(__name__, cls.__class__.__name__)) - return super(Logged, cls).__new__(cls) + + # Being a tad lazy here and not creating a Protocol. + # The actual classes define the correct type anyway + logger: typing.Any @classmethod - def __get_name(cls, *name_parts): + def __get_name(cls, *name_parts: str) -> str: return '.'.join(n.strip() for n in name_parts if n.strip()) @classmethod @functools.wraps(logging.debug) - def debug(cls, msg, *args, **kwargs): + def debug(cls, msg: str, *args: typing.Any, **kwargs: typing.Any): cls.logger.debug(msg, *args, **kwargs) @classmethod @functools.wraps(logging.info) - def info(cls, msg, *args, **kwargs): + def info(cls, msg: str, *args: typing.Any, **kwargs: typing.Any): cls.logger.info(msg, *args, **kwargs) @classmethod @functools.wraps(logging.warning) - def warning(cls, msg, *args, **kwargs): + def warning(cls, msg: str, *args: typing.Any, **kwargs: typing.Any): cls.logger.warning(msg, *args, **kwargs) @classmethod @functools.wraps(logging.error) - def error(cls, msg, *args, **kwargs): + def error(cls, msg: str, *args: typing.Any, **kwargs: typing.Any): cls.logger.error(msg, *args, **kwargs) @classmethod @functools.wraps(logging.exception) - def exception(cls, msg, *args, **kwargs): + def exception(cls, msg: str, *args: typing.Any, **kwargs: typing.Any): cls.logger.exception(msg, *args, **kwargs) @classmethod @functools.wraps(logging.log) - def log(cls, lvl, msg, *args, **kwargs): + def log(cls, lvl: int, msg: str, *args: typing.Any, **kwargs: typing.Any): cls.logger.log(lvl, msg, *args, **kwargs) + +class Logged(LoggerBase): + '''Class which automatically adds a named logger to your class when + interiting + + Adds easy access to debug, info, warning, error, exception and log methods + + >>> class MyClass(Logged): + ... def __init__(self): + ... Logged.__init__(self) + + >>> my_class = MyClass() + >>> my_class.debug('debug') + >>> my_class.info('info') + >>> my_class.warning('warning') + >>> my_class.error('error') + >>> my_class.exception('exception') + >>> my_class.log(0, 'log') + + >>> my_class._Logged__get_name('spam') + 'spam' + ''' + + logger: logging.Logger # pragma: no cover + + @classmethod + def __get_name(cls, *name_parts: str) -> str: + return LoggerBase._LoggerBase__get_name(*name_parts) # type: ignore + + def __new__(cls, *args, **kwargs): + cls.logger = logging.getLogger( + cls.__get_name(cls.__module__, cls.__name__) + ) + return super(Logged, cls).__new__(cls) diff -Nru python-utils-2.3.0/python_utils/loguru.py python-utils-3.3.3/python_utils/loguru.py --- python-utils-2.3.0/python_utils/loguru.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/python_utils/loguru.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,15 @@ +from __future__ import annotations + +from . import logger + +import loguru + +__all__ = ['Logurud'] + + +class Logurud(logger.LoggerBase): + logger: loguru.Logger + + def __new__(cls, *args, **kwargs): + cls.logger: loguru.Loguru = loguru.logger.opt(depth=1) + return super().__new__(cls) diff -Nru python-utils-2.3.0/python_utils/terminal.py python-utils-3.3.3/python_utils/terminal.py --- python-utils-2.3.0/python_utils/terminal.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/terminal.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,7 +1,10 @@ import os +import typing +from . import converters -def get_terminal_size(): # pragma: no cover + +def get_terminal_size() -> typing.Tuple[int, int]: # pragma: no cover '''Get the current size of your terminal Multiple returns are not always a good idea, but in this case it greatly @@ -11,12 +14,16 @@ Returns: width, height: Two integers containing width and height ''' + w: typing.Optional[int] + h: typing.Optional[int] try: # Default to 79 characters for IPython notebooks - from IPython import get_ipython + from IPython import get_ipython # type: ignore + ipython = get_ipython() - from ipykernel import zmqshell + from ipykernel import zmqshell # type: ignore + if isinstance(ipython, zmqshell.ZMQInteractiveShell): return 79, 24 except Exception: # pragma: no cover @@ -26,6 +33,7 @@ # This works for Python 3, but not Pypy3. Probably the best method if # it's supported so let's always try import shutil + w, h = shutil.get_terminal_size() if w and h: # The off by one is needed due to progressbars in some cases, for @@ -35,15 +43,16 @@ pass try: - w = int(os.environ.get('COLUMNS')) - h = int(os.environ.get('LINES')) + w = converters.to_int(os.environ.get('COLUMNS')) + h = converters.to_int(os.environ.get('LINES')) if w and h: return w, h except Exception: # pragma: no cover pass try: - import blessings + import blessings # type: ignore + terminal = blessings.Terminal() w = terminal.width h = terminal.height @@ -95,8 +104,10 @@ if res: import struct - (_, _, _, _, _, left, top, right, bottom, _, _) = \ - struct.unpack("hhhhHhhhhhh", csbi.raw) + + (_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack( + "hhhhHhhhhhh", csbi.raw + ) w = right - left h = bottom - top return w, h @@ -108,14 +119,21 @@ # get terminal width src: http://stackoverflow.com/questions/263890/ try: import subprocess + proc = subprocess.Popen( - ['tput', 'cols'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + ['tput', 'cols'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) output = proc.communicate(input=None) w = int(output[0]) proc = subprocess.Popen( - ['tput', 'lines'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + ['tput', 'lines'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) output = proc.communicate(input=None) h = int(output[0]) return w, h @@ -129,8 +147,10 @@ import fcntl import termios import struct + size = struct.unpack( - 'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + 'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234') + ) except Exception: return None return size diff -Nru python-utils-2.3.0/python_utils/time.py python-utils-3.3.3/python_utils/time.py --- python-utils-2.3.0/python_utils/time.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/python_utils/time.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,13 +1,18 @@ -import six +import asyncio import datetime +import functools +import itertools +import time +import python_utils +from python_utils import aio, exceptions, types # There might be a better way to get the epoch with tzinfo, please create # a pull request if you know a better way that functions for Python 2 and 3 epoch = datetime.datetime(year=1970, month=1, day=1) -def timedelta_to_seconds(delta): +def timedelta_to_seconds(delta: datetime.timedelta) -> types.Number: '''Convert a timedelta to seconds with the microseconds as fraction Note that this method has become largely obsolete with the @@ -33,7 +38,42 @@ return total -def format_time(timestamp, precision=datetime.timedelta(seconds=1)): +def delta_to_seconds(interval: types.delta_type) -> float: + ''' + Convert a timedelta to seconds + + >>> delta_to_seconds(datetime.timedelta(seconds=1)) + 1 + >>> delta_to_seconds(datetime.timedelta(seconds=1, microseconds=1)) + 1.000001 + >>> delta_to_seconds(1) + 1 + >>> delta_to_seconds('whatever') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: Unknown type ... + ''' + if isinstance(interval, datetime.timedelta): + return timedelta_to_seconds(interval) + elif isinstance(interval, (int, float)): + return interval + else: + raise TypeError('Unknown type %s: %r' % (type(interval), interval)) + + +def delta_to_seconds_or_none( + interval: types.Optional[types.delta_type], +) -> types.Optional[float]: + if interval is None: + return None + else: + return delta_to_seconds(interval) + + +def format_time( + timestamp: types.timestamp_type, + precision: datetime.timedelta = datetime.timedelta(seconds=1), +) -> str: '''Formats timedelta/datetime/seconds >>> format_time('1') @@ -58,10 +98,12 @@ ''' precision_seconds = precision.total_seconds() - if isinstance(timestamp, six.string_types + six.integer_types + (float, )): + if isinstance(timestamp, str): + timestamp = float(timestamp) + + if isinstance(timestamp, (int, float)): try: - castfunc = six.integer_types[-1] - timestamp = datetime.timedelta(seconds=castfunc(timestamp)) + timestamp = datetime.timedelta(seconds=timestamp) except OverflowError: # pragma: no cover timestamp = None @@ -71,9 +113,9 @@ seconds = seconds - (seconds % precision_seconds) return str(datetime.timedelta(seconds=seconds)) - elif isinstance(timestamp, datetime.datetime): + elif isinstance(timestamp, datetime.datetime): # pragma: no cover # Python 2 doesn't have the timestamp method - if hasattr(timestamp, 'timestamp'): # pragma: no cover + if hasattr(timestamp, 'timestamp'): seconds = timestamp.timestamp() else: seconds = timedelta_to_seconds(timestamp - epoch) @@ -82,10 +124,7 @@ seconds = seconds - (seconds % precision_seconds) try: # pragma: no cover - if six.PY3: - dt = datetime.datetime.fromtimestamp(seconds) - else: - dt = datetime.datetime.utcfromtimestamp(seconds) + dt = datetime.datetime.fromtimestamp(seconds) except ValueError: # pragma: no cover dt = datetime.datetime.max return str(dt) @@ -95,3 +134,191 @@ return '--:--:--' else: raise TypeError('Unknown type %s: %r' % (type(timestamp), timestamp)) + + +def timeout_generator( + timeout: types.delta_type, + interval: types.delta_type = datetime.timedelta(seconds=1), + iterable: types.Union[types.Iterable, types.Callable] = itertools.count, + interval_multiplier: float = 1.0, + maximum_interval: types.Optional[types.delta_type] = None, +): + ''' + Generator that walks through the given iterable (a counter by default) + until the float_timeout is reached with a configurable float_interval + between items + + >>> for i in timeout_generator(0.1, 0.06): + ... print(i) + 0 + 1 + 2 + >>> timeout = datetime.timedelta(seconds=0.1) + >>> interval = datetime.timedelta(seconds=0.06) + >>> for i in timeout_generator(timeout, interval, itertools.count()): + ... print(i) + 0 + 1 + 2 + >>> for i in timeout_generator(1, interval=0.1, iterable='ab'): + ... print(i) + a + b + + >>> timeout = datetime.timedelta(seconds=0.1) + >>> interval = datetime.timedelta(seconds=0.06) + >>> for i in timeout_generator(timeout, interval, interval_multiplier=2): + ... print(i) + 0 + 1 + 2 + ''' + float_timeout: float = delta_to_seconds(timeout) + float_interval: float = delta_to_seconds(interval) + float_maximum_interval: types.Optional[float] = delta_to_seconds_or_none( + maximum_interval + ) + + iterable_: types.Iterable + if callable(iterable): + iterable_ = iterable() + else: + iterable_ = iterable + + end = float_timeout + time.perf_counter() + for item in iterable_: + yield item + + if time.perf_counter() >= end: + break + + time.sleep(float_interval) + + interval *= interval_multiplier + if float_maximum_interval: + float_interval = min(float_interval, float_maximum_interval) + + +async def aio_timeout_generator( + timeout: types.delta_type, + interval: types.delta_type = datetime.timedelta(seconds=1), + iterable: types.Union[types.AsyncIterable, types.Callable] = aio.acount, + interval_multiplier: float = 1.0, + maximum_interval: types.Optional[types.delta_type] = None, +): + ''' + Aync generator that walks through the given iterable (a counter by + default) until the float_timeout is reached with a configurable + float_interval between items + + The interval_exponent automatically increases the float_timeout with each + run. Note that if the float_interval is less than 1, 1/interval_exponent + will be used so the float_interval is always growing. To double the + float_interval with each run, specify 2. + + Doctests and asyncio are not friends, so no examples. But this function is + effectively the same as the `timeout_generator` but it uses `async for` + instead. + ''' + float_timeout: float = delta_to_seconds(timeout) + float_interval: float = delta_to_seconds(interval) + float_maximum_interval: types.Optional[float] = delta_to_seconds_or_none( + maximum_interval + ) + + iterable_: types.AsyncIterable + if callable(iterable): + iterable_ = iterable() + else: + iterable_ = iterable + + end = float_timeout + time.perf_counter() + async for item in iterable_: # pragma: no branch + yield item + + if time.perf_counter() >= end: + break + + await asyncio.sleep(float_interval) + + float_interval *= interval_multiplier + if float_maximum_interval: # pragma: no branch + float_interval = min(float_interval, float_maximum_interval) + + +async def aio_generator_timeout_detector( + generator: types.AsyncGenerator, + timeout: types.Optional[types.delta_type] = None, + total_timeout: types.Optional[types.delta_type] = None, + on_timeout: types.Optional[types.Callable] = exceptions.reraise, + **kwargs, +): + ''' + This function is used to detect if an asyncio generator has not yielded + an element for a set amount of time. + + The `on_timeout` argument is called with the `generator`, `timeout`, + `total_timeout`, `exception` and the extra `**kwargs` to this function as + arguments. + If `on_timeout` is not specified, the exception is reraised. + If `on_timeout` is `None`, the exception is silently ignored and the + generator will finish as normal. + ''' + if total_timeout is None: + total_timeout_end = None + else: + total_timeout_end = time.perf_counter() + delta_to_seconds( + total_timeout + ) + + timeout_s = python_utils.delta_to_seconds_or_none(timeout) + + while True: + try: + if total_timeout_end and time.perf_counter() >= total_timeout_end: + raise asyncio.TimeoutError('Total timeout reached') + + if timeout_s: + yield await asyncio.wait_for(generator.__anext__(), timeout_s) + else: + yield await generator.__anext__() + + except asyncio.TimeoutError as exception: + if on_timeout is not None: + await on_timeout( + generator, timeout, total_timeout, exception, **kwargs + ) + break + + except StopAsyncIteration: + break + + +def aio_generator_timeout_detector_decorator( + timeout: types.Optional[types.delta_type] = None, + total_timeout: types.Optional[types.delta_type] = None, + on_timeout: types.Optional[types.Callable] = exceptions.reraise, + **kwargs, +): + ''' + A decorator wrapper for aio_generator_timeout_detector. + ''' + + def _timeout_detector_decorator(generator: types.Callable): + ''' + The decorator itself. + ''' + + @functools.wraps(generator) + def wrapper(*args, **wrapper_kwargs): + return aio_generator_timeout_detector( + generator(*args, **wrapper_kwargs), + timeout, + total_timeout, + on_timeout, + **kwargs, + ) + + return wrapper + + return _timeout_detector_decorator diff -Nru python-utils-2.3.0/python_utils/types.py python-utils-3.3.3/python_utils/types.py --- python-utils-2.3.0/python_utils/types.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/python_utils/types.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,135 @@ +import datetime +import decimal +from typing import * # pragma: no cover + +# import * does not import Pattern +from typing import Pattern + +# Quickhand for optional because it gets so much use. If only Python had +# support for an optional type shorthand such as `SomeType?` instead of +# `Optional[SomeType]`. +from typing import Optional as O + +# Since the Union operator is only supported for Python 3.10, we'll create a +# shorthand for it. +from typing import Union as U + +Scope = Dict[str, Any] +OptionalScope = O[Scope] +Number = U[int, float] +DecimalNumber = U[Number, decimal.Decimal] +ExceptionType = Type[Exception] +ExceptionsType = U[Tuple[ExceptionType, ...], ExceptionType] +StringTypes = U[str, bytes] + +delta_type = U[datetime.timedelta, int, float] +timestamp_type = U[ + datetime.timedelta, + datetime.date, + datetime.datetime, + str, + int, + float, + None, +] + +assert Pattern + +__all__ = [ + 'OptionalScope', + 'Number', + 'DecimalNumber', + 'delta_type', + 'timestamp_type', + # The types from the typing module. + # Super-special typing primitives. + 'Annotated', + 'Any', + 'Callable', + 'ClassVar', + 'Concatenate', + 'Final', + 'ForwardRef', + 'Generic', + 'Literal', + 'Optional', + 'ParamSpec', + 'Protocol', + 'Tuple', + 'Type', + 'TypeVar', + 'Union', + # ABCs (from collections.abc). + 'AbstractSet', # collections.abc.Set. + 'ByteString', + 'Container', + 'ContextManager', + 'Hashable', + 'ItemsView', + 'Iterable', + 'Iterator', + 'KeysView', + 'Mapping', + 'MappingView', + 'MutableMapping', + 'MutableSequence', + 'MutableSet', + 'Sequence', + 'Sized', + 'ValuesView', + 'Awaitable', + 'AsyncIterator', + 'AsyncIterable', + 'Coroutine', + 'Collection', + 'AsyncGenerator', + 'AsyncContextManager', + # Structural checks, a.k.a. protocols. + 'Reversible', + 'SupportsAbs', + 'SupportsBytes', + 'SupportsComplex', + 'SupportsFloat', + 'SupportsIndex', + 'SupportsInt', + 'SupportsRound', + # Concrete collection types. + 'ChainMap', + 'Counter', + 'Deque', + 'Dict', + 'DefaultDict', + 'List', + 'OrderedDict', + 'Set', + 'FrozenSet', + 'NamedTuple', # Not really a type. + 'TypedDict', # Not really a type. + 'Generator', + # Other concrete types. + 'BinaryIO', + 'IO', + 'Match', + 'Pattern', + 'TextIO', + # One-off things. + 'AnyStr', + 'cast', + 'final', + 'get_args', + 'get_origin', + 'get_type_hints', + 'is_typeddict', + 'NewType', + 'no_type_check', + 'no_type_check_decorator', + 'NoReturn', + 'overload', + 'ParamSpecArgs', + 'ParamSpecKwargs', + 'runtime_checkable', + 'Text', + 'TYPE_CHECKING', + 'TypeAlias', + 'TypeGuard', +] diff -Nru python-utils-2.3.0/_python_utils_tests/requirements.txt python-utils-3.3.3/_python_utils_tests/requirements.txt --- python-utils-2.3.0/_python_utils_tests/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/_python_utils_tests/requirements.txt 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1 @@ +-e .[tests] diff -Nru python-utils-2.3.0/_python_utils_tests/test_decorators.py python-utils-3.3.3/_python_utils_tests/test_decorators.py --- python-utils-2.3.0/_python_utils_tests/test_decorators.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/_python_utils_tests/test_decorators.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,38 @@ +from unittest.mock import MagicMock + +import pytest + +from python_utils.decorators import sample + + +@pytest.fixture +def random(monkeypatch): + mock = MagicMock() + monkeypatch.setattr( + "python_utils.decorators.random.random", mock, raising=True + ) + return mock + + +def test_sample_called(random): + demo_function = MagicMock() + decorated = sample(0.5)(demo_function) + random.return_value = 0.4 + decorated() + random.return_value = 0.0 + decorated() + args = [1, 2] + kwargs = {"1": 1, "2": 2} + decorated(*args, **kwargs) + demo_function.assert_called_with(*args, **kwargs) + assert demo_function.call_count == 3 + + +def test_sample_not_called(random): + demo_function = MagicMock() + decorated = sample(0.5)(demo_function) + random.return_value = 0.5 + decorated() + random.return_value = 1.0 + decorated() + assert demo_function.call_count == 0 diff -Nru python-utils-2.3.0/_python_utils_tests/test_generators.py python-utils-3.3.3/_python_utils_tests/test_generators.py --- python-utils-2.3.0/_python_utils_tests/test_generators.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/_python_utils_tests/test_generators.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,67 @@ +import asyncio + +import pytest + +import python_utils + + +@pytest.mark.asyncio +async def test_abatcher(): + async for batch in python_utils.abatcher(python_utils.acount(stop=9), 3): + assert len(batch) == 3 + + async for batch in python_utils.abatcher(python_utils.acount(stop=2), 3): + assert len(batch) == 2 + + +@pytest.mark.asyncio +async def test_abatcher_timed(): + batches = [] + async for batch in python_utils.abatcher( + python_utils.acount(stop=10, delay=0.08), interval=0.1 + ): + batches.append(batch) + + assert batches == [[0, 1, 2], [3, 4], [5, 6], [7, 8], [9]] + assert len(batches) == 5 + + +@pytest.mark.asyncio +async def test_abatcher_timed_with_timeout(): + async def generator(): + # Test if the timeout is respected + yield 0 + yield 1 + await asyncio.sleep(0.11) + + # Test if the timeout is respected + yield 2 + yield 3 + await asyncio.sleep(0.11) + + # Test if exceptions are handled correctly + await asyncio.wait_for(asyncio.sleep(1), timeout=0.05) + + # Test if StopAsyncIteration is handled correctly + yield 4 + + batcher = python_utils.abatcher(generator(), interval=0.1) + assert await batcher.__anext__() == [0, 1] + assert await batcher.__anext__() == [2, 3] + + with pytest.raises(asyncio.TimeoutError): + await batcher.__anext__() + + with pytest.raises(StopAsyncIteration): + await batcher.__anext__() + + +def test_batcher(): + batch = [] + for batch in python_utils.batcher(range(9), 3): + assert len(batch) == 3 + + for batch in python_utils.batcher(range(4), 3): + pass + + assert len(batch) == 1 diff -Nru python-utils-2.3.0/_python_utils_tests/test_import.py python-utils-3.3.3/_python_utils_tests/test_import.py --- python-utils-2.3.0/_python_utils_tests/test_import.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/_python_utils_tests/test_import.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,53 @@ +from python_utils import import_ + + +def test_import_globals_relative_import(): + for i in range(-1, 5): + relative_import(i) + + +def relative_import(level): + locals_ = {} + globals_ = {'__name__': 'python_utils.import_'} + import_.import_global('.formatters', locals_=locals_, globals_=globals_) + assert 'camel_to_underscore' in globals_ + + +def test_import_globals_without_inspection(): + locals_ = {} + globals_ = {'__name__': __name__} + import_.import_global( + 'python_utils.formatters', locals_=locals_, globals_=globals_ + ) + assert 'camel_to_underscore' in globals_ + + +def test_import_globals_single_method(): + locals_ = {} + globals_ = {'__name__': __name__} + import_.import_global( + 'python_utils.formatters', + ['camel_to_underscore'], + locals_=locals_, + globals_=globals_, + ) + assert 'camel_to_underscore' in globals_ + + +def test_import_globals_with_inspection(): + import_.import_global('python_utils.formatters') + assert 'camel_to_underscore' in globals() + + +def test_import_globals_missing_module(): + import_.import_global( + 'python_utils.spam', exceptions=ImportError, locals_=locals() + ) + assert 'camel_to_underscore' in globals() + + +def test_import_locals_missing_module(): + import_.import_global( + 'python_utils.spam', exceptions=ImportError, globals_=globals() + ) + assert 'camel_to_underscore' in globals() diff -Nru python-utils-2.3.0/_python_utils_tests/test_logger.py python-utils-3.3.3/_python_utils_tests/test_logger.py --- python-utils-2.3.0/_python_utils_tests/test_logger.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/_python_utils_tests/test_logger.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,19 @@ +import pytest + +from python_utils.loguru import Logurud + + +loguru = pytest.importorskip('loguru') + + +def test_logurud(): + class MyClass(Logurud): + pass + + my_class = MyClass() + my_class.debug('debug') + my_class.info('info') + my_class.warning('warning') + my_class.error('error') + my_class.exception('exception') + my_class.log(0, 'log') diff -Nru python-utils-2.3.0/_python_utils_tests/test_python_utils.py python-utils-3.3.3/_python_utils_tests/test_python_utils.py --- python-utils-2.3.0/_python_utils_tests/test_python_utils.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/_python_utils_tests/test_python_utils.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,9 @@ +from python_utils import __about__ + + +def test_definitions(): + # The setup.py requires this so we better make sure they exist :) + assert __about__.__version__ + assert __about__.__author__ + assert __about__.__author_email__ + assert __about__.__description__ diff -Nru python-utils-2.3.0/_python_utils_tests/test_time.py python-utils-3.3.3/_python_utils_tests/test_time.py --- python-utils-2.3.0/_python_utils_tests/test_time.py 1970-01-01 00:00:00.000000000 +0000 +++ python-utils-3.3.3/_python_utils_tests/test_time.py 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1,157 @@ +import asyncio +import itertools +from datetime import timedelta + +import pytest + +import python_utils + + +@pytest.mark.parametrize( + 'timeout,interval,interval_multiplier,maximum_interval,iterable,result', + [ + (0.2, 0.1, 0.4, 0.2, python_utils.acount, 2), + (0.3, 0.1, 0.4, 0.2, python_utils.acount(), 3), + (0.3, 0.06, 1.0, None, python_utils.acount, 5), + ( + timedelta(seconds=0.1), + timedelta(seconds=0.06), + 2.0, + timedelta(seconds=0.1), + python_utils.acount, + 2, + ), + ], +) +@pytest.mark.asyncio +async def test_aio_timeout_generator( + timeout, interval, interval_multiplier, maximum_interval, iterable, result +): + i = None + async for i in python_utils.aio_timeout_generator( + timeout, interval, iterable, maximum_interval=maximum_interval + ): + pass + + assert i == result + + +@pytest.mark.parametrize( + 'timeout,interval,interval_multiplier,maximum_interval,iterable,result', + [ + (0.01, 0.006, 0.5, 0.01, 'abc', 'c'), + (0.01, 0.006, 0.5, 0.01, itertools.count, 2), + (0.01, 0.006, 0.5, 0.01, itertools.count(), 2), + (0.01, 0.006, 1.0, None, 'abc', 'c'), + ( + timedelta(seconds=0.01), + timedelta(seconds=0.006), + 2.0, + timedelta(seconds=0.01), + itertools.count, + 2, + ), + ], +) +def test_timeout_generator( + timeout, interval, interval_multiplier, maximum_interval, iterable, result +): + i = None + for i in python_utils.timeout_generator( + timeout=timeout, + interval=interval, + interval_multiplier=interval_multiplier, + iterable=iterable, + maximum_interval=maximum_interval, + ): + pass + + assert i == result + + +@pytest.mark.asyncio +async def test_aio_generator_timeout_detector(): + async def generator(): + for i in range(10): + await asyncio.sleep(i / 100.0) + yield i + + detector = python_utils.aio_generator_timeout_detector + # Test regular timeout with reraise + with pytest.raises(asyncio.TimeoutError): + async for i in detector(generator(), 0.05): + pass + + # Test regular timeout with clean exit + async for i in detector(generator(), 0.05, on_timeout=None): + pass + + assert i == 4 + + # Test total timeout with reraise + with pytest.raises(asyncio.TimeoutError): + async for i in detector(generator(), total_timeout=0.1): + pass + + # Test total timeout with clean exit + async for i in detector(generator(), total_timeout=0.1, on_timeout=None): + pass + + assert i == 4 + + # Test stop iteration + async for i in detector(generator(), on_timeout=None): + pass + + +@pytest.mark.asyncio +async def test_aio_generator_timeout_detector_decorator(): + # Test regular timeout with reraise + @python_utils.aio_generator_timeout_detector_decorator(timeout=0.05) + async def generator(): + for i in range(10): + await asyncio.sleep(i / 100.0) + yield i + + with pytest.raises(asyncio.TimeoutError): + async for i in generator(): + pass + + # Test regular timeout with clean exit + @python_utils.aio_generator_timeout_detector_decorator( + timeout=0.05, on_timeout=None + ) + async def generator(): + for i in range(10): + await asyncio.sleep(i / 100.0) + yield i + + async for i in generator(): + pass + + assert i == 4 + + # Test total timeout with reraise + @python_utils.aio_generator_timeout_detector_decorator(total_timeout=0.1) + async def generator(): + for i in range(10): + await asyncio.sleep(i / 100.0) + yield i + + with pytest.raises(asyncio.TimeoutError): + async for i in generator(): + pass + + # Test total timeout with clean exit + @python_utils.aio_generator_timeout_detector_decorator( + total_timeout=0.1, on_timeout=None + ) + async def generator(): + for i in range(10): + await asyncio.sleep(i / 100.0) + yield i + + async for i in generator(): + pass + + assert i == 4 diff -Nru python-utils-2.3.0/README.rst python-utils-3.3.3/README.rst --- python-utils-2.3.0/README.rst 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/README.rst 2022-05-31 21:30:38.000000000 +0000 @@ -1,8 +1,8 @@ Useful Python Utils ============================================================================== -.. image:: https://travis-ci.org/WoLpH/python-utils.svg?branch=master - :target: https://travis-ci.org/WoLpH/python-utils +.. image:: https://github.com/WoLpH/python-utils/actions/workflows/main.yml/badge.svg?branch=master + :target: https://github.com/WoLpH/python-utils/actions/workflows/main.yml .. image:: https://coveralls.io/repos/WoLpH/python-utils/badge.svg?branch=master :target: https://coveralls.io/r/WoLpH/python-utils?branch=master @@ -28,23 +28,30 @@ Requirements for installing: ------------------------------------------------------------------------------ - - `six` any recent version +For the Python 3+ release (i.e. v3.0.0 or higher) there are no requirements. +For the Python 2 compatible version (v2.x.x) the `six` package is needed. Installation: ------------------------------------------------------------------------------ The package can be installed through `pip` (this is the recommended method): +.. code-block:: bash + pip install python-utils Or if `pip` is not available, `easy_install` should work as well: +.. code-block:: bash + easy_install python-utils Or download the latest release from Pypi (https://pypi.python.org/pypi/python-utils) or Github. Note that the releases on Pypi are signed with my GPG key (https://pgp.mit.edu/pks/lookup?op=vindex&search=0xE81444E9CE1F695D) and can be checked using GPG: +.. code-block:: bash + gpg --verify python-utils-.tar.gz.asc python-utils-.tar.gz Quickstart @@ -57,22 +64,138 @@ Examples ------------------------------------------------------------------------------ -To extract a number from nearly every string: +Automatically converting a generator to a list, dict or other collections +using a decorator: -.. code-block:: python +.. code-block:: pycon + + >>> @decorators.listify() + ... def generate_list(): + ... yield 1 + ... yield 2 + ... yield 3 + ... + >>> generate_list() + [1, 2, 3] + + >>> @listify(collection=dict) + ... def dict_generator(): + ... yield 'a', 1 + ... yield 'b', 2 + + >>> dict_generator() + {'a': 1, 'b': 2} + +Retrying until timeout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To easily retry a block of code with a configurable timeout, you can use the +`time.timeout_generator`: + +.. code-block:: pycon + + >>> for i in time.timeout_generator(10): + ... try: + ... # Run your code here + ... except Exception as e: + ... # Handle the exception + +Formatting of timestamps, dates and times +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Easy formatting of timestamps and calculating the time since: + +.. code-block:: pycon + + >>> time.format_time('1') + '0:00:01' + >>> time.format_time(1.234) + '0:00:01' + >>> time.format_time(1) + '0:00:01' + >>> time.format_time(datetime.datetime(2000, 1, 2, 3, 4, 5, 6)) + '2000-01-02 03:04:05' + >>> time.format_time(datetime.date(2000, 1, 2)) + '2000-01-02' + >>> time.format_time(datetime.timedelta(seconds=3661)) + '1:01:01' + >>> time.format_time(None) + '--:--:--' + + >>> formatters.timesince(now) + 'just now' + >>> formatters.timesince(now - datetime.timedelta(seconds=1)) + '1 second ago' + >>> formatters.timesince(now - datetime.timedelta(seconds=2)) + '2 seconds ago' + >>> formatters.timesince(now - datetime.timedelta(seconds=60)) + '1 minute ago' + +Converting your test from camel-case to underscores: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: pycon + + >>> camel_to_underscore('SpamEggsAndBacon') + 'spam_eggs_and_bacon' + +Attribute setting decorator. Very useful for the Django admin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A convenient decorator to set function attributes using a decorator: + +.. code-block:: pycon + + You can use: + >>> @decorators.set_attributes(short_description='Name') + ... def upper_case_name(self, obj): + ... return ("%s %s" % (obj.first_name, obj.last_name)).upper() + + Instead of: + >>> def upper_case_name(obj): + ... return ("%s %s" % (obj.first_name, obj.last_name)).upper() - from python_utils import converters + >>> upper_case_name.short_description = 'Name' - number = converters.to_int('spam15eggs') - assert number == 15 +This can be very useful for the Django admin as it allows you to have all +metadata in one place. - number = converters.to_int('spam') - assert number == 0 +Scaling numbers between ranges +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - number = converters.to_int('spam', default=1) - assert number == 1 +.. code-block:: pycon - number = converters.to_float('spam1.234') + >>> converters.remap(500, old_min=0, old_max=1000, new_min=0, new_max=100) + 50 + + # Or with decimals: + >>> remap(decimal.Decimal('250.0'), 0.0, 1000.0, 0.0, 100.0) + Decimal('25.0') + +Get the screen/window/terminal size in characters: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: pycon + + >>> terminal.get_terminal_size() + (80, 24) + +That method supports IPython and Jupyter as well as regular shells, using +`blessings` and other modules depending on what is available. + +Extracting numbers from nearly every string: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: pycon + + >>> converters.to_int('spam15eggs') + 15 + >>> converters.to_int('spam') + 0 + >>> number = converters.to_int('spam', default=1) + 1 + +Doing a global import of all the modules in a package programmatically: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To do a global import programmatically you can use the `import_global` function. This effectively emulates a `from ... import *` @@ -84,6 +207,9 @@ # The following is the equivalent of `from some_module import *` import_global('some_module') +Automatically named logger for classes: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Or add a correclty named logger to your classes which can be easily accessed: .. code-block:: python @@ -106,3 +232,39 @@ import logging my_class.log(logging.ERROR, 'log') +Alternatively loguru is also supported. It is largely a drop-in replacement for the logging module which is a bit more convenient to configure: + +First install the extra loguru package: + +.. code-block:: bash + + pip install 'python-utils[loguru]' + +.. code-block:: python + + class MyClass(Logurud): + ... + +Now you can use the `Logurud` class to make functions such as `self.info()` +available. The benefit of this approach is that you can add extra context or +options to you specific loguru instance (i.e. `self.logger`): + +Convenient type aliases and some commonly used types: + +.. code-block:: python + + # For type hinting scopes such as locals/globals/vars + Scope = Dict[str, Any] + OptionalScope = O[Scope] + + # Note that Number is only useful for extra clarity since float + # will work for both int and float in practice. + Number = U[int, float] + DecimalNumber = U[Number, decimal.Decimal] + + # To accept an exception or list of exceptions + ExceptionType = Type[Exception] + ExceptionsType = U[Tuple[ExceptionType, ...], ExceptionType] + + # Matching string/bytes types: + StringTypes = U[str, bytes] diff -Nru python-utils-2.3.0/requirements.txt python-utils-3.3.3/requirements.txt --- python-utils-2.3.0/requirements.txt 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/requirements.txt 2022-05-31 21:30:38.000000000 +0000 @@ -0,0 +1 @@ +. diff -Nru python-utils-2.3.0/setup.cfg python-utils-3.3.3/setup.cfg --- python-utils-2.3.0/setup.cfg 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/setup.cfg 2022-05-31 21:30:38.000000000 +0000 @@ -1,3 +1,6 @@ +[aliases] +test=pytest + [metadata] description-file = README.rst @@ -12,8 +15,6 @@ pdb=1 # pdb-failures=1 - - [build_sphinx] source-dir = docs/ build-dir = docs/_build @@ -28,3 +29,7 @@ [upload] sign = 1 +[flake8] +per-file-ignores = + python_utils/types.py: F403,F405 +exclude = diff -Nru python-utils-2.3.0/setup.py python-utils-3.3.3/setup.py --- python-utils-2.3.0/setup.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/setup.py 2022-05-31 21:30:38.000000000 +0000 @@ -1,33 +1,53 @@ import os +import typing + import setuptools # To prevent importing about and thereby breaking the coverage info we use this # exec hack -about = {} +about: typing.Dict[str, str] = {} with open('python_utils/__about__.py') as fp: exec(fp.read(), about) - if os.path.isfile('README.rst'): long_description = open('README.rst').read() else: long_description = 'See http://pypi.python.org/pypi/python-utils/' - if __name__ == '__main__': setuptools.setup( - name=about['__package_name__'], + python_requires='>3.6.0', + name='python-utils', version=about['__version__'], author=about['__author__'], author_email=about['__author_email__'], description=about['__description__'], url=about['__url__'], license='BSD', - packages=setuptools.find_packages(), + packages=setuptools.find_packages( + exclude=['_python_utils_tests', '*.__pycache__'] + ), long_description=long_description, - install_requires=['six'], tests_require=['pytest'], - setup_requires=['pytest-runner'], + extras_require={ + 'loguru': [ + 'loguru', + ], + 'docs': [ + 'mock', + 'sphinx', + 'python-utils', + ], + 'tests': [ + 'flake8', + 'pytest', + 'pytest-cov', + 'pytest-mypy', + 'pytest-asyncio', + 'sphinx', + 'types-setuptools', + 'loguru', + ], + }, classifiers=['License :: OSI Approved :: BSD License'], ) - diff -Nru python-utils-2.3.0/tests/requirements.txt python-utils-3.3.3/tests/requirements.txt --- python-utils-2.3.0/tests/requirements.txt 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/tests/requirements.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ --r ../requirements.txt -flake8 -pytest -pytest-cache -pytest-cov -pytest-flakes -pytest-pep8 -sphinx diff -Nru python-utils-2.3.0/tests/test_import.py python-utils-3.3.3/tests/test_import.py --- python-utils-2.3.0/tests/test_import.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/tests/test_import.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,50 +0,0 @@ -from python_utils import import_ - - -def test_import_globals_relative_import(): - for i in range(-1, 5): - relative_import(i) - - -def relative_import(level): - locals_ = {} - globals_ = {'__name__': 'python_utils.import_'} - import_.import_global('.formatters', locals_=locals_, globals_=globals_) - import pprint - pprint.pprint(globals_) - assert 'camel_to_underscore' in globals_ - - -def test_import_globals_without_inspection(): - locals_ = {} - globals_ = {'__name__': __name__} - import_.import_global( - 'python_utils.formatters', locals_=locals_, globals_=globals_) - assert 'camel_to_underscore' in globals_ - - -def test_import_globals_single_method(): - locals_ = {} - globals_ = {'__name__': __name__} - import_.import_global( - 'python_utils.formatters', ['camel_to_underscore'], locals_=locals_, - globals_=globals_) - assert 'camel_to_underscore' in globals_ - - -def test_import_globals_with_inspection(): - import_.import_global('python_utils.formatters') - assert 'camel_to_underscore' in globals() - - -def test_import_globals_missing_module(): - import_.import_global( - 'python_utils.spam', exceptions=ImportError, locals_=locals()) - assert 'camel_to_underscore' in globals() - - -def test_import_locals_missing_module(): - import_.import_global( - 'python_utils.spam', exceptions=ImportError, globals_=globals()) - assert 'camel_to_underscore' in globals() - diff -Nru python-utils-2.3.0/tests/test_python_utils.py python-utils-3.3.3/tests/test_python_utils.py --- python-utils-2.3.0/tests/test_python_utils.py 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/tests/test_python_utils.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -from python_utils import __about__ - - -def test_definitions(): - # The setup.py requires this so we better make sure they exist :) - assert __about__.__version__ - assert __about__.__author__ - assert __about__.__author_email__ - assert __about__.__description__ - diff -Nru python-utils-2.3.0/tox.ini python-utils-3.3.3/tox.ini --- python-utils-2.3.0/tox.ini 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/tox.ini 2022-05-31 21:30:38.000000000 +0000 @@ -1,25 +1,32 @@ [tox] -envlist = py27, py33, py34, py35, py36, pypy, flake8, docs +envlist = black, py37, py38, py39, py310, py311, pypy3, flake8, docs skip_missing_interpreters = True [testenv] basepython = - py27: python2.7 - py34: python3.4 - py35: python3.5 - py36: python3.6 + py37: python3.7 + py38: python3.8 + py39: python3.9 + py310: python3.10 + py311: python3.11 pypy: pypy -deps = -r{toxinidir}/tests/requirements.txt -commands = python setup.py pytest {posargs} +setenv = PY_IGNORE_IMPORTMISMATCH=1 +deps = -r{toxinidir}/_python_utils_tests/requirements.txt +commands = py.test --basetemp="{envtmpdir}" --confcutdir=.. {posargs} python_utils _python_utils_tests + +[testenv:black] +basepython = python3 +deps = black +commands = black --skip-string-normalization --line-length 79 {toxinidir}/setup.py {toxinidir}/_python_utils_tests {toxinidir}/python_utils [testenv:flake8] -basepython = python2.7 +basepython = python3 deps = flake8 -commands = flake8 --ignore=W391 python_utils {posargs} +commands = flake8 python_utils {posargs} [testenv:docs] -basepython = python2.7 +basepython = python3 whitelist_externals = rm cd @@ -30,6 +37,11 @@ mkdir -p docs/_static sphinx-apidoc -o docs/ python_utils rm -f docs/modules.rst - sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html {posargs} + sphinx-build -n -W -b html -d docs/_build/doctrees docs docs/_build/html {posargs} deps = -r{toxinidir}/docs/requirements.txt +[flake8] +ignore = W391, W503, E741, E203 +exclude = + docs + diff -Nru python-utils-2.3.0/.travis.yml python-utils-3.3.3/.travis.yml --- python-utils-2.3.0/.travis.yml 2018-02-11 22:57:23.000000000 +0000 +++ python-utils-3.3.3/.travis.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -sudo: false -language: python - -env: - global: - - PIP_WHEEL_DIR=$HOME/.wheels - - PIP_FIND_LINKS=file://$PIP_WHEEL_DIR - -matrix: - include: - - python: '2.7' - env: TOXENV=docs - - python: '2.7' - env: TOXENV=flake8 - - python: '2.7' - env: TOXENV=py27 - - python: '3.4' - env: TOXENV=py34 - - python: '3.5' - env: TOXENV=py35 - - python: '3.6' - env: TOXENV=py36 - - python: 'pypy' - env: TOXENV=pypy - -cache: - directories: - - $HOME/.wheels - -# command to install dependencies, e.g. pip install -r requirements.txt -install: - - mkdir -p $PIP_WHEEL_DIR - - pip wheel -r tests/requirements.txt - - pip install -e . - - pip install tox coveralls - -script: - - tox - -after_success: - - coveralls - -notifications: - email: - on_success: never - on_failure: change -