Repository: peterdemin/pip-compile-multi Branch: master Commit: ffb2003af628 Files: 146 Total size: 284.1 KB Directory structure: gitextract_snhzpegr/ ├── .coveragerc ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── pipcompilemulti.yml │ ├── python310-windows.yml │ ├── python310.yml │ ├── python311.yml │ ├── python312.yml │ ├── python313.yml │ └── python314.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .pylintrc ├── .readthedocs.yml ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── Dockerfile ├── HISTORY.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs/ │ ├── Makefile │ ├── _static/ │ │ └── custom.css │ ├── afterword.rst │ ├── boilerplate.rst │ ├── conf.py │ ├── features.rst │ ├── history.rst │ ├── index.rst │ ├── installation.rst │ ├── migration.rst │ ├── precommit.rst │ └── why.rst ├── how-to.md ├── nested/ │ ├── base.in │ ├── base.txt │ ├── diamond.in │ ├── diamond.txt │ ├── subproject/ │ │ ├── base.in │ │ ├── base.txt │ │ ├── sub.in │ │ └── sub.txt │ ├── up.in │ └── up.txt ├── pipcompilemulti/ │ ├── __init__.py │ ├── actions.py │ ├── cli_v1.py │ ├── cli_v2.py │ ├── config.py │ ├── deduplicate.py │ ├── dependency.py │ ├── discover.py │ ├── environment.py │ ├── features/ │ │ ├── __init__.py │ │ ├── add_hashes.py │ │ ├── annotate_index.py │ │ ├── autoresolve.py │ │ ├── backtracking.py │ │ ├── base.py │ │ ├── base_dir.py │ │ ├── build_isolation.py │ │ ├── compatible.py │ │ ├── controller.py │ │ ├── emit_find_links.py │ │ ├── emit_trusted_host.py │ │ ├── extra_index_url.py │ │ ├── file_extensions.py │ │ ├── forbid_post.py │ │ ├── forward.py │ │ ├── header.py │ │ ├── limit_in_paths.py │ │ ├── live_output.py │ │ ├── skip_constraint_comments.py │ │ ├── strip_extras.py │ │ ├── unsafe.py │ │ ├── upgrade.py │ │ ├── use_cache.py │ │ └── use_uv.py │ ├── options.py │ ├── utils.py │ └── verify.py ├── requirements/ │ ├── base.hash │ ├── base.in │ ├── base.txt │ ├── local.hash │ ├── local.in │ ├── local.txt │ ├── test.hash │ ├── test.in │ ├── test.txt │ ├── testwin.hash │ ├── testwin.in │ └── testwin.txt ├── setup.cfg ├── setup.py ├── tests/ │ ├── __init__.py │ ├── configs/ │ │ ├── pyproject.toml │ │ ├── requirements.ini │ │ └── setup.cfg │ ├── conflicting-in-merge/ │ │ ├── base1.in │ │ ├── base1.txt │ │ ├── base2.in │ │ ├── base2.txt │ │ ├── together.in │ │ └── together.txt │ ├── conflicting-in-ref/ │ │ ├── base1.in │ │ ├── base1.txt │ │ ├── base2.in │ │ └── base2.txt │ ├── conftest.py │ ├── sys_platform/ │ │ ├── base.in │ │ ├── base.txt │ │ ├── test.in │ │ └── test.txt │ ├── test_add_hashes.py │ ├── test_cli_v1.py │ ├── test_cli_v2.py │ ├── test_config.py │ ├── test_conflicts.py │ ├── test_deduplicate.py │ ├── test_dependency.py │ ├── test_discover.py │ ├── test_pipcompilemulti.py │ ├── test_skip_constraint_comments.py │ ├── test_upgrade_feature.py │ ├── test_utils.py │ ├── upgrade/ │ │ ├── base.in │ │ └── base.txt │ ├── upgrade-autoresolve-with-range/ │ │ ├── base.in │ │ ├── base.txt │ │ ├── prod.in │ │ └── prod.txt │ ├── upgrade-autoresolve-with-range-expected/ │ │ ├── base.in │ │ ├── base.txt │ │ ├── prod.in │ │ └── prod.txt │ ├── upgrade-expected/ │ │ ├── base.in │ │ └── base.txt │ ├── upgrade-with-range/ │ │ ├── base.in │ │ └── base.txt │ ├── upgrade-with-range-expected/ │ │ ├── base.in │ │ └── base.txt │ └── utils.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ # .coveragerc to control coverage.py [run] branch = True [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: ignore_errors = True [html] directory = htmlcov ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab ================================================ FILE: .github/workflows/pipcompilemulti.yml ================================================ name: Update Dependencies on: schedule: - cron: '15 15 * * 3' workflow_dispatch: {} jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install tox run: | python -m pip install --upgrade pip pip install tox - name: Update dependencies run: | tox -e upgrade - name: Create commits run: | git config user.name 'pip-compile-multi' git config user.email 'pip-compile-multi@users.noreply.github.com' git commit -am "Update dependencies" - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> token: ${{ secrets.PAT }} title: Update dependencies body: | Auto-generated by [pip-compile-multi](https://github.com/peterdemin/pip-compile-multi) branch: update-dependencies branch-suffix: timestamp ================================================ FILE: .github/workflows/python310-windows.yml ================================================ name: Python 3.10 Windows on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: windows-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install tox run: | python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | tox -e py310-windows ================================================ FILE: .github/workflows/python310.yml ================================================ name: Python 3.10 on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install tox run: | python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | tox ================================================ FILE: .github/workflows/python311.yml ================================================ name: Python 3.11 on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.11 uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install tox run: | python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | tox -e py311-linux || true ================================================ FILE: .github/workflows/python312.yml ================================================ name: Python 3.12 on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.12 uses: actions/setup-python@v4 with: python-version: '3.12' - name: Install tox run: | python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | tox -e py312-linux || true ================================================ FILE: .github/workflows/python313.yml ================================================ name: Python 3.13 on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.13 uses: actions/setup-python@v4 with: python-version: '3.13' - name: Install tox run: | python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | tox -e py313-linux || true ================================================ FILE: .github/workflows/python314.yml ================================================ name: Python 3.14 on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.14 uses: actions/setup-python@v4 with: python-version: '3.14' - name: Install tox run: | python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | tox -e py314-linux || true ================================================ FILE: .gitignore ================================================ *.py[cod] /.env/ /.venv*/ # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml /.pytest_cache/ # Conflict tests side-effects /conflicting-in-*/*.txt # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Complexity output/*.html output/*/index.html # Sphinx docs/_build # Cookiecutter output/ boilerplate/ /.cache/ /.eggs/ # Vim Session.vim /htmlcov/ /.idea/ *.swp ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/peterdemin/pip-compile-multi rev: v2.6.2 hooks: - id: pip-compile-multi-verify - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 hooks: - id: flake8 - id: trailing-whitespace ================================================ FILE: .pre-commit-hooks.yaml ================================================ - id: pip-compile-multi-verify name: pip-compile-multi verify language: python entry: pip-compile-multi verify files: ^requirements/ pass_filenames: false require_serial: true types: [file, non-executable, text] ================================================ FILE: .pylintrc ================================================ [MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. jobs=1 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable=bad-inline-option, consider-using-f-string, deprecated-pragma, file-ignored, locally-disabled, no-else-return, raw-checker-failed, suppressed-message, useless-object-inheritance, useless-suppression # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio).You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=optparse.Values,sys.exit [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=100 # Maximum number of lines in a module max-module-lines=1000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [SPELLING] # Limits count of emitted suggestions for spelling mistakes max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [BASIC] # Naming style matching correct argument names argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style #argument-rgx= # Naming style matching correct attribute names attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style #attr-rgx= # Bad variable names which should always be refused, separated by a comma bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style #class-attribute-rgx= # Naming style matching correct class names class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming-style #class-rgx= # Naming style matching correct constant names const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style # Allow long test names function-rgx=[a-z_][a-z0-9_]{2,70}$ # Good variable names which should always be accepted, separated by a comma good-names=i, j, k, ex, Run, _, fp, logger, options # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Naming style matching correct inline iteration names inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style #inlinevar-rgx= # Naming style matching correct method names method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style #method-rgx= # Naming style matching correct module names module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. property-classes=abc.abstractproperty # Naming style matching correct variable names variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style #variable-rgx= [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [DESIGN] # Maximum number of arguments for function / method max-args=500 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in a if statement max-bool-expr=5 # Maximum number of branch for function / method body max-branches=12 # Maximum number of locals for function / method body max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of statements in function / method body max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=builtins.Exception ================================================ FILE: .readthedocs.yml ================================================ # .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: all # Optionally set the version of Python and requirements required to build your docs build: os: ubuntu-22.04 tools: python: "3.9" commands: - pip install -r requirements/local.hash - pip install -e . - make -C docs html - mkdir -p $READTHEDOCS_OUTPUT/ - mv docs/_build/html $READTHEDOCS_OUTPUT/ ================================================ FILE: .travis.yml ================================================ sudo: false language: python python: - "3.6" - "3.7" - "3.8" matrix: include: - os: linux python: 3.6 env: TOXENV=lint before_install: - pip install codecov coveralls install: pip install tox-travis script: tox after_success: - codecov - coveralls deploy: provider: pypi user: peterdemin password: secure: "ZZTWaBiHQicobKw/xiD6IGQoAdrGmZ/DAXXitbnTE4jGpDKq1X+0JDNbYo1EVXQ/r+wmL3nBTz1DPrEymo//hli/4GzffargF7lnWg7GfrUSAxWUSWj36+AAyRLWGqwhSPQCxDymAi6TJF5VSKw1qDrs7vicjSOwtXnUxBANMaBxeFxhJJbhQpViBCTs6w5ZsPRFaWIVkxdOSFQY09ZgDBU17VYaVyzB5okZ5Ogk+3Xj5cqZKPDrptmuYSjdHm/LEj5q6gesuOOYTXfloqLirSTxPedo+gLUHoUIDgvPhNm1VJiiBx+d5DjIKhsOeV385rGYWUq92EznfrdPmorAmh62KkP4pTL1+Jd9gcS/FIXiMk3ga9tQLH51dqkRRgwgM5HMVYZIyl+D2I2Nw6RdMcViJZkv8VV5wF/58JiHdIdiutgo0Y14dtbUjryDoz5Ivsgp2NodyA/AJ97NCf8CVrYxOJivjEJ5tkP/ANGzg3/gDTy3t1qVRBaNyDRQK77TwbvpR9LWODZXKuSh7WXeZpflSYT1PL8mLtBgho0onzXPjmgyYgGMEDEAx+kaXc5xLj2uH1Z+RC7bXbYkeAxjnpkxwWKNcCnnpELE56732JYqdDh8Pjc6eJtPCS4mAFxNPAafR4wl6NNWOgT4vjnh6ZJAGeFBIKw21AWsYI99mc4=" on: tags: true distributions: "sdist bdist_wheel" skip_existing: true ================================================ FILE: AUTHORS.rst ================================================ ======= Credits ======= Development Lead ---------------- * Peter Demin Contributors ------------ None yet. Why not be the first? ================================================ FILE: CONTRIBUTING.rst ================================================ ============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/peterdemin/pip-compile-multi/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ pip-compile-multi could always use more documentation, whether as part of the official pip-compile-multi docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/peterdemin/pip-compile-multi/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up `pip-compile-multi` for local development. 1. Fork the `pip-compile-multi` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/pip-compile-multi.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ mkvirtualenv pip-compile-multi $ cd pip-compile-multi/ $ python setup.py develop 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ flake8 pip-compile-multi.py test_pip-compile-multi.py $ py.test $ tox To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 2.6, 2.7, 3.3, and 3.4, and for PyPy. Check https://travis-ci.org/peterdemin/pip-compile-multi/pull_requests and make sure that the tests pass for all supported Python versions. Tips ---- To run a subset of tests:: TODO ================================================ FILE: Dockerfile ================================================ FROM python:3.10 RUN apt-get update && apt-get install -y build-essential WORKDIR /pcm ================================================ FILE: HISTORY.rst ================================================ History ======= 3.3.1 (2025-04-19) ------------------ * Add support for `--emit-find-links/--no-emit-find-links`. (Issue `#554`_, PR `#556`_, thanks to `Aidas Bendoraitis`_.) .. _#554: https://github.com/peterdemin/pip-compile-multi/issues/554 .. _#556: https://github.com/peterdemin/pip-compile-multi/pull/556 .. _Aidas Bendoraitis: https://github.com/archatas 3.3.0 (2025-03-25) ------------------ * Add Python 3.14 to supported versions. * Remove Python 3.9 from supported versions. * Using Python 3.10 as the base version going forward. 3.2.2 (2025-06-19) ------------------ * Fix ``TypeError`` is not triggered when sections is None. (Issue `#516`_, PR `#517`_, thanks to `Nikola Trandafilovic`_.) .. _#516: https://github.com/peterdemin/pip-compile-multi/issues/516 .. _#517: https://github.com/peterdemin/pip-compile-multi/pull/517 .. _Nikola Trandafilovic: https://github.com/elrik 3.2.1 (2025-06-19) ------------------ * Fix a bug when loading configuration from ``pyproject.toml`` that doesn't have ``[tool.requirements]`` section. 3.2.0 (2025-06-17) ------------------ * Enable annotations for uv resolver. (Requires uv>=0.7) (Issue `#505`_) .. _#505: https://github.com/peterdemin/pip-compile-multi/issues/505 3.1.0 (2025-05-23) ------------------ * Add support for ``pyproject.toml`` for ``requirements`` configuration. (Issue `#496`_, PR `#498`_, thanks to `Abir A`_.) .. _#496: https://github.com/peterdemin/pip-compile-multi/issues/496 .. _#498: https://github.com/peterdemin/pip-compile-multi/pull/498 .. _Abir A: https://github.com/Some7hing0riginal 3.0.0 (2025-04-23) ------------------ * Add second-generation CLI entrypoint ``requirements``. * Make ``--use-cache`` enabled by default. * Make ``--skip-constraints`` enabled by default. * Removed deprecated ``-n, --only-name`` option. Use ``-t, --only-path`` instead. 2.8.0 (2024-11-27) ------------------ * Add ``--uv`` flag to use UV instead of pip-tools. (PR `#483`_, thanks to `Mustafa Tevfik Kadan`_). .. _#483: https://github.com/peterdemin/pip-compile-multi/pull/483 .. _Mustafa Tevfik Kadan: https://github.com/ktevfik 2.7.0 (2024-11-27) ------------------ * Remove Python 3.8 from tested versions. Using Python 3.9 as the base version going forward. 2.6.4 (2023-06-06) ------------------ * Add ``--strip-extras`` pass-through flag. (PR `#455`_, thanks to `Tim Vergenz`_). .. _#455: https://github.com/peterdemin/pip-compile-multi/pull/455 .. _Tim Vergenz: https://github.com/vergenzt 2.6.3 (2023-05-05) ------------------ * Allow version constraints in ``--upgrade-package`` parameters. (Issue `#392`_, PR `#394`_, thanks to `Peter Law`_). .. _#392: https://github.com/peterdemin/pip-compile-multi/issues/392 .. _#394: https://github.com/peterdemin/pip-compile-multi/pull/394 .. _Peter Law: https://github.com/PeterJCLaw 2.6.2 (2023-02-23) ------------------ * Fix package name normalization for names with delimiters (``urltemplate`` != ``url-template``). 2.6.1 (2022-11-23) ------------------ * Add ``--backtracking/--no-backtracking`` flag (`backtracking docs `_). (Issue `#345`_, PR `#360`_) * Bumped ``pip-tools`` minimum version constraint to ``6.8.0`` to support ``--resolver`` option. .. _#345: https://github.com/peterdemin/pip-compile-multi/issues/345 .. _#360: https://github.com/peterdemin/pip-compile-multi/pull/360 2.6.0 (2022-11-23) ------------------ * Fix cross-env package names matching when they use delimiters (-_.) and upper/lower case. 2.5.0 (2022-11-03) ------------------ * Add ``--emit_trusted_host/--no-emit_trusted_host`` flag (`trusted host annotation docs `_). Thanks to `Phil Blackwood`_ (Issue `#351`_, PR `#353`_). * Remove Python 3.6 and 3.7 from tested versions (package works, but maintaining CI becomes problematic). Using Python 3.8 as the base version going forward. .. _Phil Blackwood: https://github.com/philblckwd .. _#351: https://github.com/peterdemin/pip-compile-multi/issues/351 .. _#353: https://github.com/peterdemin/pip-compile-multi/pull/353 2.4.6 (2022-07-22) ------------------ * Add support for `PEP-426: environment markers `_. 2.4.5 (2022-04-01) ------------------ * Fix ``--build-isolation`` flag passing. Thanks to `Jake Schmidt`_ (Issue `#312`_, PR `#313`_). .. _#312: https://github.com/peterdemin/pip-compile-multi/issues/312 .. _#313: https://github.com/peterdemin/pip-compile-multi/pull/313 2.4.4 (2022-03-30) ------------------ * Add ``--build-isolation`` flag. Thanks to `Jake Schmidt`_ (PR `#311`_). * Fix ``--skip-constraints`` feature for URL dependencies. .. _Jake Schmidt: https://github.com/schmidt-jake .. _#311: https://github.com/peterdemin/pip-compile-multi/pull/311 2.4.3 (2022-01-19) ------------------ * Prioritize URL depedencies when parsing to enable URLs that have version pins. * Make ``--autoresolve`` work correctly with ``-t`` and ``-n`` options. * Add support for "package @ url" notation. 2.4.2 (2021-09-01) ------------------ * Fix ``--autoresolve`` when ``--directory="."``. 2.4.1 (2021-04-02) ------------------ * Add ``--live/--no-live`` option to control when to print debug output. Thanks to `John Sandall`_ and `Thomas Grainger`_ (Issue `#153`_, PRs `#247`_ and `#251`_). * Add ``--extra-index-url `` option. Thanks to `Erik Jan de Vries`_ (PRs `#250`_ and `#252`_). .. _#153: https://github.com/peterdemin/pip-compile-multi/issues/153 .. _#247: https://github.com/peterdemin/pip-compile-multi/pull/247 .. _#250: https://github.com/peterdemin/pip-compile-multi/pull/250 .. _#251: https://github.com/peterdemin/pip-compile-multi/pull/251 .. _#252: https://github.com/peterdemin/pip-compile-multi/pull/252 .. _John Sandall: https://github.com/john-sandall .. _Thomas Grainger: https://github.com/graingert .. _Erik Jan de Vries: https://github.com/erikjandevries 2.4.0 (2021-03-17) ------------------ * Update --index/--no-index to --emit-index-url/--no-emit-index-url for compatibility with pip-tools 6.0 (Issue `#243`_). .. _#243: https://github.com/peterdemin/pip-compile-multi/issues/243 2.3.2 (2021-02-18) ------------------ * Fix cross-feature logic for --autoresolve and --upgrade-package (PR `#236`_). .. _#236: https://github.com/peterdemin/pip-compile-multi/pull/236 2.3.1 (2021-02-16) ------------------ * Fix for a bug introduced in 2.2.2 when running pip-compile-multi installed for Python 3, and having ``python`` symlinked to Python 2 (Issue `#233`_, PR `#234`_). .. _#233: https://github.com/peterdemin/pip-compile-multi/issues/233 .. _#234: https://github.com/peterdemin/pip-compile-multi/pull/234 2.3.0 (2021-02-04) ------------------ * Make SHA1 hashes of input files in a more robust way (Issue `#215`_). Now it ignores changes to comments, whitespace and order of packages. .. _#215: https://github.com/peterdemin/pip-compile-multi/issues/215 2.2.2 (2021-01-29) ------------------ * Add support for calling using `python -m pipcompilemulti.cli_v1` notation. 2.2.1 (2021-01-29) ------------------ * Add ``--skip-constraints`` option. * Fix bootstrapping for autoresolve case with missing output files. 2.2.0 (2020-01-22) ------------------ * Add ``--autoresolve`` option for conflict-free compilations (PR #224). * Auto-discover requirements in other directories by following references (PR #221). * Add support for new-style multiline "via" comments from pip-tools (PR #222). 2.1.0 (2020-08-19) ------------------ * Update dependencies. * Revert relative path normalization, introduced in #167 (thanks to @john-bodley #200). 2.0.0 (2020-05-18) ------------------ * Drop Python 2.7 support. pip-tools 4 no longer works with the latest pip, there's no way to continue Python 2.7 support. 1.5.9 (2020-03-23) ------------------ * Remove directory path from "via" annotations (thanks to @HALtheWise #166 #167). 1.5.8 (2019-09-27) ------------------ * Add option ``--annotate-index`` (thanks to @john-bodley #160). 1.5.7 (2019-09-27) ------------------ * Enable accidentially disabled ``--upgrade`` option. .. _1.5.6: 1.5.6 (2019-09-18) ------------------ * Minor fixes to packaging and documentation. Warning: this version is broken and won't pass ``--upgrade`` option to ``pip-compile``. If you have this version installed, you need to manually upgrade it. For example, using command:: pip-compile-multi --upgrade-package pip-compile-multi Like in this `PR `_. 1.5.4 (2019-09-16) ------------------ * Fixed MANIFEST to include features directory Warning: this version is broken and won't pass ``--upgrade`` option to ``pip-compile``. See notes for 1.5.6_ for details. 1.5.3 (2019-09-14) ------------------ * Refactored features to separate modules. * Allow passing verify options after verify command. * Trim irrelevant entries from the traceback. Warning: this version is broken and won't install ``features`` directory. See notes for 1.5.6_ for details. 1.5.2 (2019-09-12) ------------------ * Added option ``--allow-unsafe``. (thanks to @mozbhearsum #157). 1.5.1 (2019-08-08) ------------------ * Added option ``--use-cache``. (thanks to @kolotev #149). 1.5.0 (2019-08-06) ------------------ * Changed short option for ``--forbid-post`` from ``-P`` to ``-p`` (as it conflicted with ``-P`` for ``--upgrade-package`` #147). 1.3.1 (2019-02-19) ------------------ * Re-removed workaround for future[s] packages in Python3 1.3.0 (2018-12-27) ------------------ * Introduced CLI v2 (disabled by default) 1.2.2 (2018-11-20) ------------------ * Removed workaround for future[s] packages in Python3 (no longer needed) 1.2.1 (2018-04-16) ------------------- * Fixed Restructured text formatting (thanks to @yigor) * Updated test dependencies (and hashes) 1.2.0 (2018-04-03) ------------------- * Added --forbid-post option 1.1.12 (2018-02-23) ------------------- * Added checks for conflicting package versions * Added support for VCS dependencies * Added --no-upgrade option 1.1.11 (2018-02-09) ------------------- * Propagate --only-name option to references * Fixed extension override options 1.1.10 (2018-02-09) ------------------- * Added ``--generate-hashes`` option 1.1.9 (2018-02-08) ------------------ * Fixed directory override option * Added --only-name option 1.1.8 (2018-01-25) ------------------ * Fixed comment justification 1.1.6 (2018-01-19) ------------------ * Added ``pip-compile-multi verify`` command 1.1.5 (2018-01-16) ------------------ * Omit future[s] packages for Python3 1.1.0 (2018-01-12) ------------------ * Added files discovery. 1.0.0 (2018-01-11) ------------------ * First release on PyPI. ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) [year] [fullname] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include AUTHORS.rst include CONTRIBUTING.rst include HISTORY.rst include LICENSE include requirements/base.in ================================================ FILE: Makefile ================================================ .PHONY: virtual_env_set virtual_env_set: ifndef VIRTUAL_ENV $(error VIRTUAL_ENV not set) endif ### DEPENDENCIES ### .PHONY: install install: requirements/local.hash virtual_env_set pip install -r requirements/local.hash pip install -e . --no-deps .PHONY: sync sync: requirements/local.hash virtual_env_set pip-sync requirements/local.hash pip install -e . --no-deps .PHONY: %-docker %-docker: ## Could be lock-docker or upgrade-docker docker run --rm -it -v $(PWD):/pcm $$(docker build -q .) /usr/bin/make $*-ubuntu .PHONY: %-ubuntu %-ubuntu: .venv3 .venv3/bin/python3 -m pip install tox .venv3/bin/python3 -m tox -e $* .venv3: python3.10 -m venv .venv3 .PHONY: lock lock: virtual_env_set tox -e lock .PHONY: upgrade upgrade: virtual_env_set tox -e upgrade ### CI ### .PHONY: test test: tox .PHONY: clean clean: rm -rf build dist pip-compile-multi.egg-info docs/_build rm -rf .venv3 find . -name "*.pyc" -delete find * -type d -name '__pycache__' | xargs rm -rf .PHONY: build build: clean python setup.py sdist bdist_wheel .PHONY: release release: build twine upload dist/* ### MISC ### .PHONY: docs docs: virtual_env_set make -C docs html ================================================ FILE: README.rst ================================================ ================= pip-compile-multi ================= .. image:: https://badge.fury.io/py/pip-compile-multi.png :target: https://badge.fury.io/py/pip-compile-multi .. image:: https://github.com/peterdemin/pip-compile-multi/actions/workflows/python38.yml/badge.svg :target: https://github.com/peterdemin/pip-compile-multi/actions/workflows/python38.yml .. image:: https://img.shields.io/pypi/pyversions/pip-compile-multi.svg :target: https://pypi.python.org/pypi/pip-compile-multi `Docs `_ Compile multiple requirements files to lock dependency versions. Install ------- .. code-block:: shell pip install pip-compile-multi Run ---- .. code-block:: shell pip-compile-multi Trusted by ---------- |uber| |mozilla| |twitter| |nih| |skydio| |pallets| |moveworks| Help needed ----------- The following issues have the highest impact for the project. Contributions are welcome! 1. `Pull requirements from pyproject.toml `_ 2. `Productionize GitHub Action to update dependencies on schedule `_ Your mission, should you choose to accept it, is to comment on the issue you want to work on, and open a PR. I'll review/merge the PR in a timely fashion, and release a new version with your name in the `changelog `_. Read the Docs ------------- * `Why use pip-compile-multi `_ * `How to start using pip-compile-multi `_ * `List of features `_ .. |nih| image:: docs/NIH_logo.svg :width: 200 px :height: 200 px :target: https://www.nih.gov/ .. |uber| image:: docs/Uber_Logo_Black_RGB.svg :width: 200 px :height: 200 px :target: https://www.uber.com/ .. |mozilla| image:: docs/moz-logo-bw-rgb.svg :width: 200 px :height: 200 px :target: https://www.mozilla.org/ .. |skydio| image:: docs/skydio-logo-black.svg :width: 200 px :height: 200 px :target: https://www.skydio.com/ .. |pallets| image:: docs/pallets.png :width: 200 px :height: 200 px :target: https://palletsprojects.com/ .. |twitter| image:: docs/twitter_logo.svg :width: 200 px :height: 200 px :target: https://twitter.com/ .. |moveworks| image:: docs/Moveworks.svg :width: 400 px :height: 200 px :target: https://moveworks.com/ ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # 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) ================================================ FILE: docs/_static/custom.css ================================================ body { font-family: 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; } div.document { width: 71em; } ================================================ FILE: docs/afterword.rst ================================================ Have fun! --------- Now that occasional backward incompatible dependency release can't ruin your day, you can **spread the word** about ``pip-compile-multi``, ask for a new feature in a `GitHub issue`_, or even open a PR ;-). .. _GitHub issue: https://github.com/peterdemin/pip-compile-multi/issues ================================================ FILE: docs/boilerplate.rst ================================================ Bonus: boilerplate to put in project's README --------------------------------------------- Nice way of introducing dependency management process to new team members for copy-pasting to `README.md`: .. code-block:: text ## Dependency management This project uses [pip-compile-multi](https://pypi.org/project/pip-compile-multi/) for hard-pinning dependencies versions. Please see its documentation for usage instructions. In short, `requirements/base.in` contains the list of direct requirements with occasional version constraints (like `Django~=4.0`) and `requirements/base.txt` is automatically generated from it by adding recursive tree of dependencies with fixed versions. The same goes for `test` and `dev`. To add a new dependency, add it to the appropriate environment's `in` file, (e.g. `requirements/base.in`) and run `pip-compile-multi --no-upgrade`. To upgrade dependency versions, run `pip-compile-multi`. For installation always use `.txt` files. For example, command `pip install -e . -r requirements/dev.txt` will install this project in development mode, testing requirements and development tools. Another useful command is `pip-sync requirements/dev.txt`, it uninstalls packages from your virtualenv that aren't listed in the file. If project is using the second-generation ``requirements`` CLI: .. code-block:: text ## Dependency management This project uses [pip-compile-multi](https://pypi.org/project/pip-compile-multi/) for hard-pinning dependencies versions. Please see its documentation for usage instructions. In short, `requirements/base.in` contains the list of direct requirements with occasional version constraints (like `Django~=4.0`) and `requirements/base.txt` is automatically generated from it by adding recursive tree of dependencies with fixed versions. The same goes for `test` and `dev`. To add a new dependency, add it to the appropriate environment's `in` file, (e.g. `requirements/base.in`) and run `requirements lock`. To upgrade dependency versions, run `requirements upgrade`. For installation always use `.txt` files. For example, command `pip install -Ue . -r requirements/dev.txt` will install this project in development mode, testing requirements and development tools. Another useful command is `pip-sync requirements/dev.txt`, it uninstalls packages from your virtualenv that aren't listed in the file. ================================================ FILE: docs/conf.py ================================================ """ Configuration file for the Sphinx documentation builder. This file does only contain a selection of the most common options. For a full list see the documentation: http://www.sphinx-doc.org/en/master/config """ # pylint: disable=invalid-name,redefined-builtin # -- Path setup -------------------------------------------------------------- import os import sys sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- project = 'pip-compile-multi' copyright = '2019, Peter Demin' author = 'Peter Demin' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '1.5.1' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # 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', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- 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 = 'alabaster' # 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 = { 'github_user': 'peterdemin', 'github_repo': 'pip-compile-multi', 'github_type': 'star', 'github_banner': True, 'github_button': True, 'extra_nav_links': { 'Code': 'https://github.com/peterdemin/pip-compile-multi', 'Releases': 'https://pypi.org/project/pip-compile-multi/', 'Issue tracker': 'https://github.com/peterdemin/pip-compile-multi/issues', }, } # 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'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. html_sidebars = { '**': [ 'about.html', 'navigation.html', 'searchbox.html', 'sourcelink.html', ] } # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'pip-compile-multidoc' # -- 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': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'pip-compile-multi.tex', 'pip-compile-multi Documentation', 'Peter Demin', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'pip-compile-multi', 'pip-compile-multi Documentation', [author], 1) ] # -- 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 = [ (master_doc, 'pip-compile-multi', 'pip-compile-multi Documentation', author, 'pip-compile-multi', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # 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 list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/', None), } ================================================ FILE: docs/features.rst ================================================ Features -------- ``pip-compile-multi`` supports many options to customize compilation. Each option can be specified in requirements configuration file, by replacing dashes with underscores. For example, `--use-cache` becomes `use_cache`. Supported configuration files: ``pyproject.toml``, ``requirements.ini``, ``setup.cfg``, ``tox.ini``. In INI files, use section name starting with ``requirements:``, for example: ``[requirements:Python 3]`` In ``pyproject.toml`` prefix section name with ``tool.requirements``, for example: ``[tool.requirements.python3]`` or ``[tool.requirements]``. .. automodule:: pipcompilemulti.features.base_dir .. automodule:: pipcompilemulti.features.file_extensions .. automodule:: pipcompilemulti.features.upgrade .. automodule:: pipcompilemulti.features.use_cache .. automodule:: pipcompilemulti.features.compatible .. automodule:: pipcompilemulti.features.add_hashes .. automodule:: pipcompilemulti.features.unsafe .. automodule:: pipcompilemulti.features.header .. automodule:: pipcompilemulti.features.limit_in_paths .. automodule:: pipcompilemulti.features.annotate_index .. automodule:: pipcompilemulti.features.extra_index_url .. automodule:: pipcompilemulti.features.emit_trusted_host .. automodule:: pipcompilemulti.features.emit_find_links .. automodule:: pipcompilemulti.features.autoresolve .. automodule:: pipcompilemulti.features.backtracking .. automodule:: pipcompilemulti.features.skip_constraint_comments .. automodule:: pipcompilemulti.features.strip_extras .. automodule:: pipcompilemulti.features.live_output .. automodule:: pipcompilemulti.features.use_uv .. automodule:: pipcompilemulti.verify ================================================ FILE: docs/history.rst ================================================ .. include:: ../HISTORY.rst ================================================ FILE: docs/index.rst ================================================ .. pip-compile-multi documentation master file, created by sphinx-quickstart on Thu Aug 8 15:36:37 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. pip-compile-multi ~~~~~~~~~~~~~~~~~ Pip-compile-multi is a command line utility, that compiles multiple requirements files to lock dependency versions. Underneath it uses `pip-tools`_ or uv_ for actual compilation. Pip-compile-multi targets complex projects and provides high level of automation and flexibility. .. _pip-tools: https://github.com/jazzband/pip-tools .. _uv: https://docs.astral.sh/uv/ To install: .. code-block:: shell pip install pip-compile-multi To run: .. code-block:: shell pip-compile-multi Introduced in 3.0.0, the new CLI reads configuration from ``pyproject.toml`` (or one of INI files: ``requirements.ini``, ``setup.cfg``, ``tox.ini``): .. code-block:: shell requirements [lock|upgrade|verify] Why use pip-compile-multi? ========================== .. toctree:: :maxdepth: 2 why How to use pip-compile-multi? ============================= .. toctree:: :maxdepth: 2 installation migration features precommit boilerplate Release notes ============= .. toctree:: :maxdepth: 2 history .. include:: afterword.rst ================================================ FILE: docs/installation.rst ================================================ Installation ------------ Python Version ============== We recommend using the latest version of Python 3. Pip-compile-multi supports Python 3.5 and newer, Python 2.7, and PyPy. Dependencies ============ These distributions will be installed automatically when installing pip-compile-multi. * `Click`_ is a framework for writing command line applications. * `pip-tools`_ is a set of command line tools to help you keep your pip-based packages fresh, even when you've pinned them. * `toposort`_ implements topological sort algorithm. Pip-compile-multi uses it to compose compilation order of requirements files. .. _Click: https://palletsprojects.com/p/click/ .. _pip-tools: https://github.com/jazzband/pip-tools .. _toposort: https://pypi.org/project/toposort/ Virtual environments ==================== Use a virtual environment to manage the dependencies for your project, both in development and in production. What problem does a virtual environment solve? The more Python projects you have, the more likely it is that you need to work with different versions of Python libraries, or even Python itself. Newer versions of libraries for one project can break compatibility in another project. Virtual environments are independent groups of Python libraries, one for each project. Packages installed for one project will not affect other projects or the operating system's packages. Python 3 comes bundled with the :mod:`venv` module to create virtual environments. If you're using a modern version of Python, you can continue on to the next section. If you're using Python 2, see :ref:`install-install-virtualenv` first. .. _install-create-env: Create an environment ~~~~~~~~~~~~~~~~~~~~~ Create a project folder and a :file:`venv` folder within: .. code-block:: sh $ mkdir myproject $ cd myproject $ python3 -m venv venv On Windows: .. code-block:: bat $ py -3 -m venv venv If you needed to install virtualenv because you are using Python 2, use the following command instead: .. code-block:: sh $ python2 -m virtualenv venv On Windows: .. code-block:: bat > \Python27\Scripts\virtualenv.exe venv .. _install-activate-env: Activate the environment ~~~~~~~~~~~~~~~~~~~~~~~~ Before you work on your project, activate the corresponding environment: .. code-block:: sh $ . venv/bin/activate On Windows: .. code-block:: bat > venv\Scripts\activate Your shell prompt will change to show the name of the activated environment. Install pip-compile-multi ========================= Within the activated environment, use the following command to install pip-compile-multi: .. code-block:: shell pip install pip-compile-multi pip-compile-multi is now installed. Check out the :doc:`/features` or go to the :doc:`Documentation Overview `. .. _install-install-virtualenv: Install virtualenv ================== If you are using Python 2, the venv module is not available. Instead, install `virtualenv`_. On Linux, virtualenv is provided by your package manager: .. code-block:: sh # Debian, Ubuntu $ sudo apt-get install python-virtualenv # CentOS, Fedora $ sudo yum install python-virtualenv # Arch $ sudo pacman -S python-virtualenv If you are on Mac OS X or Windows, download `get-pip.py`_, then: .. code-block:: sh $ sudo python2 Downloads/get-pip.py $ sudo python2 -m pip install virtualenv On Windows, as an administrator: .. code-block:: bat > \Python27\python.exe Downloads\get-pip.py > \Python27\python.exe -m pip install virtualenv Now you can return above and :ref:`install-create-env`. .. _virtualenv: https://virtualenv.pypa.io/ .. _get-pip.py: https://bootstrap.pypa.io/get-pip.py ================================================ FILE: docs/migration.rst ================================================ How to start using pip-compile-multi on existing project -------------------------------------------------------- Initial situation ================= There are various ways to declare dependencies in Python project. The most straightforward is to just put them right into ``setup.py``, like `Flask`_ does. Another common option is to have one or more ``requirements.txt`` files in a project, like `Deluge`_ have. .. _Flask: https://github.com/pallets/flask/blob/master/setup.py#L52-L75 .. _Deluge: https://github.com/deluge-torrent/deluge/blob/develop/requirements.txt Migration steps =============== 1. Create ``requirements`` directory. 2. Copy-paste the list of project runtime dependencies to ``requirements/base.in``. 3. Create ``requirements/test.in`` with test time dependencies. Make sure it has a reference to runtime dependencies - ``-r base.in``. 4. Run ``pip-compile-multi``. It will produce two more files: * ``requirements/base.txt`` * ``requirements/test.txt`` 5. :ref:`Unpin ` packages in ``.in`` files. 6. Run ``pip-compile-multi`` again to upgrade the compiled files. .. _unpin: How to unpin packages ===================== No constraints ~~~~~~~~~~~~~~ Some projects don't constraint it's dependencies. In this case, there's nothing to unpin, no more work needed. Hard-pinned versions (==) ~~~~~~~~~~~~~~~~~~~~~~~~~ Some projects hard-pin all dependencies, just to be safe. Most likely, the code will run fine with the next patch release, but it's hard to tell for sure. For such cases, comprehensive test suite is vital. Your milage may vary, but generally the fastest workflow is as follows: 1. Remove all the constraints, by deleting everything after package names (e.g. ``==1.2.3``). 2. Recompile ``.txt`` requirements. 3. Install new versions. 4. Run tests. 5. If tests passed, job's done. 6. If some of the tests fails, it's likely that some of the original constraints was indeed required. Try to find what's the incompatible package version, maybe read package's CHANGELOG. Maybe the simpliest way is to return whatever constraint was originally set to move the needle. 7. After updating one of the ``.in`` files, go to step 2. Two-way constraints (>1,<2) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The popular requirements policy is to add packages constrained to minor releases, like this:: uwsgi>2.0.0,<2.1.0 The idea is that dependency is following SemVer and patch release won't break any functionality. Of course, the reality is that some times patch release introduces a bug, and some times next major release is backwards compatible. But looking at this line you can't tell if ``uwsgi==2.1.0`` is going to break your app. The only way to know it is to actually try. So for such projects the general approach is to remove all ``<`` constraints and see which of them were really needed. ================================================ FILE: docs/precommit.rst ================================================ Verify as pre-commit hook ========================= To verify that ``pip-compile-multi`` has been run after changing ``.in`` files as a `PreCommit`_ hook, just add the following to your local repo's ``.pre-commit-config.yaml`` file: .. code-block:: yaml - repo: https://github.com/peterdemin/pip-compile-multi rev: v2.6.2 hooks: - id: pip-compile-multi-verify .. _PreCommit: https://pre-commit.com/ ================================================ FILE: docs/why.rst ================================================ Motivation ---------- I will start from the very basics of dependency management and will go very slow, so if you feel bored, just scroll to the next section. Suppose you have a python project with following direct dependencies: .. code-block:: text click pip-tools (Yes I took pip-compile-multi as an example). Let's save them as-is in ``requirements/base.in``. Those are unpinned libraries. It means that whenever developer runs .. code-block:: shell pip install -r requirements/base.in they will get *some* version of these libraries. And the chances are that if several developers do the same over some period, some will have different dependency versions than others. Also, if the project is online service, one day it may stop working after redeployment because some of the dependencies had backward incompatible release. These backward incompatible changes are relatively common. To avoid this problem, Python developers are hard-pinning (aka locking) their dependencies. So instead of a list of libraries, they have something like: .. code-block:: text click==6.7 pip-tools==1.11.0 (To keep things neat let's put this into ``requirements/base.txt``) That's good for a starter. But there are two significant drawbacks: 1. Developers have to do non-trivial operations if they want to keep up with newer versions (that have bug fixes and performance improvements). 2. Indirect dependencies (that is dependencies of dependencies) may still have backward-incompatible releases, that break everything. Let's put aside point 1 and fight point 2. Let's do .. code-block:: shell pip freeze > requirements/base.txt Now we have full hierarchy of dependencies hard-pinned: .. code-block:: text click==6.7 first==2.0.1 pip-tools==1.11.0 six==1.11.0 That's great, and solves the main problem - service will be deployed exactly [1]_ the same every single time and all developers will have same environments. .. [1] That's not true. Someone could re-upload broken package under existing version on PyPI. For 100% reproducible builds use hashes. This case is so common that there already are some tools to solve it. Two worth mentioning are: 1. `Pip Tools`_ - a mature package that is enhanced by ``pip-compile-multi``. 2. `PipEnv`_ - a fresh approach that is going to become the "official" Python way of locking dependencies some day. .. _Pip Tools: https://github.com/jazzband/pip-tools .. _PipEnv: https://github.com/pypa/pipenv But what if the project uses some packages that are not required by the service itself? For example ``pytest``, that is needed to run unit tests, but should never be deployed to a production site. Or ``flake8`` - syntax checking tool. If they are installed in the current virtual environment, they will get into ``pip freeze`` output. That's no good. And removing them manually from ``requirements/base.txt`` is not an option. But still, these packages must be pinned to ensure, that tests are running the same way on all development machines (and build server). So let's get hands dirty and put all the testing stuff into ``requirements/test.in``: .. code-block:: text -r base.in prospector pylint flake8 mock six Note, how I put ``-r base.in`` in the beginning, so that *test* dependencies are installed along with the *base*. Now installation command is .. code-block:: shell pip install -r requirements/test.in For one single time (exceptionally to show how unacceptable is this task) let's manually compose ``requirements/test.txt``. After installation, run freeze to bring the whole list of all locked packages: .. code-block:: shell $ pip freeze astroid==1.6.0 click==6.7 dodgy==0.1.9 first==2.0.1 flake8==3.5.0 flake8-polyfill==1.0.2 isort==4.2.15 lazy-object-proxy==1.3.1 mccabe==0.6.1 mock==2.0.0 pbr==3.1.1 pep8-naming==0.5.0 pip-tools==1.11.0 prospector==0.12.7 pycodestyle==2.0.0 pydocstyle==2.1.1 pyflakes==1.6.0 pylint==1.8.1 pylint-celery==0.3 pylint-common==0.2.5 pylint-django==0.7.2 pylint-flask==0.5 pylint-plugin-utils==0.2.6 PyYAML==3.12 requirements-detector==0.5.2 setoptconf==0.2.0 six==1.11.0 snowballstemmer==1.2.1 wrapt==1.10.11 Wow! That's quite a list! But we remember what goes into base.txt: 1. click 2. first 3. pip-tools 4. six Good, everything else can be put into ``requirements/test.txt``. But wait, ``six`` is included in ``test.in`` and is missing in ``test.txt``. That feels wrong. Ah, it's because we've moved ``six`` to the ``base.txt``. It's good that we didn't forget, that it should be in *base*. We might forget next time though. Why don't we automate it? That's what ``pip-compile-multi`` is for. Managing dependency versions in multiple environments ----------------------------------------------------- Let's rehearse. Example service has two groups of dependencies (or, as I call them, environments): .. code-block:: shell $ cat requirements/base.in click pip-tools $ cat requirements/test.in -r base.in prospector pylint flake8 mock six To make automation even more appealing, let's add one more environment. I'll call it *local* - things that are needed during development, but are not required by tests, or service itself. .. code-block:: shell $ cat requirements/local.in -r test.in tox Now we want to put all *base* dependencies along with all their recursive dependencies in ``base.txt``, all recursive *test* dependencies except for *base* into ``test.txt``, and all recursive *local* dependencies except for *base* and *test* into ``local.txt``. .. code-block:: shell $ pip-compile-multi Locking requirements/base.in to requirements/base.txt. References: [] Locking requirements/test.in to requirements/test.txt. References: ['base'] Locking requirements/local.in to requirements/local.txt. References: ['base', 'test'] Yes, that's right. All the tedious dependency versions management job done with a single command that doesn't even have options. Now you can run ``git diff`` to review the changes and ``git commit`` to save them. To install the new set of versions run: .. code-block:: shell pip install -Ur requirements/local.txt It's a perfect time to run all the tests and make sure, that updates were backward compatible enough for your needs. More often than I'd like in big projects, it's not so. Let's say the new version of ``pylint`` dropped support of old Python version, that you still need to support. Than you open ``test.in`` and soft-pin it with descriptive comment: .. code-block:: shell $ cat requirements/test.in -r base.in prospector pylint<1.8 # Newer versions dropped support for Python 2.4 flake8 mock six I know, this example is made up. But you get the idea. That re-run ``pip-compile-multi`` to compile new ``test.txt`` and check new set. Benefits -------- I want to summarise, why ``pip-compile-multi`` might be a good addition to your project. Some of the benefits are achievable with other methods, but I want to be general: 1. Production will not suddenly break after redeployment because of backward incompatible dependency release. 2. Every development machine will have the same package versions. 3. Service still uses most recent versions of packages. And fresh means best here. 4. Dependencies are upgraded when the time is suitable for the service, not whenever they are released. 5. Different environments are separated into different files. 6. ``*.in`` files are small and manageable because they store only direct dependencies. 7. ``*.txt`` files are exhaustive and precise (but you don't need to edit them). ================================================ FILE: how-to.md ================================================ # Managing dependencies in multi-platform Python project I'm going to describe the setup I made for my Python [project](https://github.com/peterdemin/pip-compile-multi). The project supports Python version 2.7 and 3.4+, PyPy, Linux, and Windows. It runs tests on every commit in Travis CI and AppVeyor. The project relies on a few runtime packages (5), a bunch for testing (12) and a lot for development (36). Some dependencies are shared, some specific to Python 2.7 and some to Windows. The project uses hard-pinned packages with hashes to verify the integrity of installed versions. ## Problems Let's summarize what we are solving: 1. The project has 3 set of dependencies: base, test and local which are needed respectfully during running, testing and developing. 2. Some packages are needed only under Python 2.7. 3. Other packages are needed only under Windows. 4. All packages must be hard-pinned and hashed. Alternatively, if turned into tasks: 1. Organize dependencies for each environment. 2. Orchestrate installation in different environments. ## Solutions The first tool we are using is [pip-compile-multi](https://github.com/peterdemin/pip-compile-multi), which is ~~ironically~~ the project we are using as an example. It has verbose documentation, so I'll briefly outline how it is applied here. There are 6 `.in` files in the `requirements` directory: base.in, test.in, local.in, py27.in, local27.in, testwin.in. `base`, `test` and `local` are meant to be used under Python 3. `py27` and `local27` are holding Python 2.7 backports of Python 3 packages and version constraints for projects, which dropped Python 2 support in newer versions. `testwin` has a single entry: `colorama`, which is `pytest` dependency that is installed only under windows. First, we are pinning packages to the current versions with the following command: ``` $ pip-compile-multi -n local -n testwin ``` It produces files `base.txt`, `test.txt`, `local.txt` and `testwin.txt` with recursively retrieved hard-pinned package versions. The second command takes these `.txt` files and produce `.hash` files with attached package hashes: ``` $ pip-compile-multi -n local -n testwin -g local -g testwin -i txt -o hash ``` The same operation must be repeated separately for Python 2.7 packages: ``` $ pip-compile-multi -n py27 -n local27 $ pip-compile-multi -n py27 -n local27 -g py27 -g local27 -i txt -o hash ``` Separation is required because [pip-tools](https://github.com/jazzband/pip-tools) can't traverse packages that are not required in the current runtime. To automate this tasks, I'm using the second tool - [tox](https://tox.readthedocs.io/en/latest/). Here is my configuration: ``` [testenv:upgrade2] basepython = python2.7 deps = pip-compile-multi commands = pip-compile-multi -n py27 -n local27 pip-compile-multi -n py27 -n local27 -g py27 -g local27 -i txt -o hash [testenv:upgrade3] basepython = python3.6 deps = pip-compile-multi commands = pip-compile-multi -n local -n testwin pip-compile-multi -n local -n testwin -g local -g testwin -i txt -o hash ``` To run it, I execute: ``` $ tox -e upgrade3 -e upgrade2 ``` To run unit and doc tests locally I have somewhat complex testenv setup: ``` [tox] envlist = py{27,34,35,36,37}-{linux,windows} [testenv] platform = linux: linux windows: win32 deps = linux: -r{toxinidir}/requirements/test.hash windows: -r{toxinidir}/requirements/testwin.hash py27: -r{toxinidir}/requirements/py27.hash commands = python -m pytest --doctest-modules pipcompilemulti.py test_pipcompilemulti.py ``` The setup says to use `test.hash` file under Linux, `testwin.hash` under Windows and add `py27.hash` if it is also running under Python 2.7. AppVeyor runs these tests under Windows; its configuration file defines which Python version to use and what parameters to pass to `tox.ini`: ``` environment: matrix: - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.8" PYTHON_ARCH: "32" TOX_ENV: "py27" - PYTHON: "C:\\Python34" PYTHON_VERSION: "3.4.1" PYTHON_ARCH: "32" TOX_ENV: "py34" - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5.4" PYTHON_ARCH: "32" TOX_ENV: "py35" - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6.4" PYTHON_ARCH: "32" TOX_ENV: "py36" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" install: - "appveyor/setup_build_env.cmd" - "powershell appveyor/install.ps1" build: false test_script: - "%PYTHON%/Scripts/tox -e %TOX_ENV%-windows" ``` `environment.matrix` defines `TOX_ENV` variable, which is passed to `tox` in `test_script` step. Travis CI has the following configuration to run tests under Linux: ``` # Config file for automatic testing at travis-ci.org language: python python: - "3.7-dev" - "3.6" - "3.5" - "3.4" - "2.7" - "pypy" # command to install dependencies install: - ./install_test_deps.sh - pip install -e . # command to run tests using coverage, e.g., python setup.py test script: python -m pytest --doctest-modules pipcompilemulti.py test_pipcompilemulti.py ``` I stashed dependency installation step into bash script `install_test_deps.sh`: ``` #!/bin/sh python --version 2>&1 | grep -q 'Python 3' if [ $? -eq 0 ] then # Python 3 exec pip install -r requirements/test.hash else # Python 2 or PyPy exec pip install -r requirements/test.hash -r requirements/py27.hash fi ``` I could have reused tox here too with [tox-travis](https://github.com/tox-dev/tox-travis). Someday, maybe, I will. ## Conclusion Python is multi-platform language, but it lacks built-in tools for secure management of dependencies versions. Tools like `pip-compile-multi` and `tox` accompanied by CI services like `Travis-ci` and `AppVeyor` significantly reduce the effort. However, correct configuration takes time and requires skills and persistence. Described solution can be used as a boilerplate for project setup, or as guidance for building another tool that puts a framework for complex dependency management. ================================================ FILE: nested/base.in ================================================ -r subproject/sub.in ================================================ FILE: nested/base.txt ================================================ # SHA1:6a7fb5b91341e12583307fb55069246d43096b44 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r subproject/sub.txt ================================================ FILE: nested/diamond.in ================================================ -r base.in -r subproject/base.in ================================================ FILE: nested/diamond.txt ================================================ # SHA1:1301840447a3dce149692692b41974b7668ba22e # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r base.txt -r subproject/base.txt ================================================ FILE: nested/subproject/base.in ================================================ -r ../up.in ================================================ FILE: nested/subproject/base.txt ================================================ # SHA1:e975ad2caba20cf1afaea2cd3626c6c566edfacf # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r ../up.txt ================================================ FILE: nested/subproject/sub.in ================================================ -r base.in ================================================ FILE: nested/subproject/sub.txt ================================================ # SHA1:a87fd594461015e819a1f468943967d12880b85d # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r base.txt ================================================ FILE: nested/up.in ================================================ six ================================================ FILE: nested/up.txt ================================================ # SHA1:bec9703f7a456cd2b4ab5fb3220ae016e3e394e3 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # six==1.15.0 # via -r nested/up.in ================================================ FILE: pipcompilemulti/__init__.py ================================================ """Pip compile multi aka requirements""" __author__ = 'Peter Demin' __email__ = 'peterdemin@gmail.com' __version__ = '3.3.1' ================================================ FILE: pipcompilemulti/actions.py ================================================ #!/usr/bin/env python """High level actions to be called from CLI""" import logging from .discover import discover from .environment import Environment from .verify import generate_robust_hash_comment from .features import FEATURES from .deduplicate import PackageDeduplicator logger = logging.getLogger("pip-compile-multi") def recompile(): """Compile requirements files for all environments.""" env_confs = FEATURES.on_discover( discover(FEATURES.compose_input_file_path('*')) ) deduplicator = PackageDeduplicator() deduplicator.on_discover(env_confs) sink_in_path = FEATURES.sink_in_path() if sink_in_path: sink_env = Environment(in_path=sink_in_path) logger.info( "Creating a temporary file with all dependencies at %s", sink_env.outfile, ) sink_env.create_lockfile() compile_topologically(env_confs, deduplicator) def compile_topologically(env_confs, deduplicator): """Compile environments in topological order of reference.""" for conf in env_confs: env = Environment(in_path=conf['in_path'], deduplicator=deduplicator) if env.maybe_create_lockfile(): # Only munge lockfile if it was written. header_text = generate_robust_hash_comment(env.infile) + FEATURES.get_header_text() env.replace_header(header_text) env.add_references(conf['refs']) ================================================ FILE: pipcompilemulti/cli_v1.py ================================================ """The current stable version of command line interface.""" import os import sys import logging from traceback import print_exception import click from .actions import recompile from .verify import verify_environments from .features import FEATURES THIS_FILE = os.path.abspath(__file__) @click.group(invoke_without_command=True) @click.pass_context @FEATURES.bind def cli(ctx): """Recompile""" logging.basicConfig(level=logging.DEBUG, format="%(message)s") sys.excepthook = exception_hook if ctx.invoked_subcommand is None: recompile() @cli.command() @click.pass_context @FEATURES.bind def verify(ctx): """ For each environment verify hash comments and report failures. If any failure occured, exit with code 1. """ sys.excepthook = exception_hook ctx.exit(0 if verify_environments() else 1) def exception_hook(exctype, value, traceback): """Strip exception printout above this module.""" print_exception(exctype, value, trim_traceback(traceback)) def trim_traceback(traceback): """Trim traceback top so it starts with this module. Return original traceback if this module is not found. """ level = 0 new_traceback = traceback while new_traceback is not None: file_path = new_traceback.tb_frame.f_code.co_filename if THIS_FILE.startswith(file_path): return new_traceback level += 1 new_traceback = new_traceback.tb_next return traceback if __name__ == '__main__': cli() # pylint: disable=no-value-for-parameter ================================================ FILE: pipcompilemulti/cli_v2.py ================================================ """Human-friendly interface to pip-compile-multi""" import functools import logging import click from .actions import recompile from .config import read_config, read_sections from .options import OPTIONS from .verify import verify_environments logger = logging.getLogger("pip-compile-multi") DEFAULT_OPTIONS = { 'directory': 'requirements', 'in_ext': 'in', 'out_ext': 'txt', 'autoresolve': True, } @click.group() def cli(): """Human-friendly interface to pip-compile-multi""" logging.basicConfig(level=logging.INFO, format="%(message)s") @cli.command() def lock(): """Lock new dependencies without upgrading.""" run_configurations(recompile, read_config, upgrade=False) @cli.command() @click.argument('packages', nargs=-1) def upgrade(packages): """Upgrade locked dependency versions.""" run_configurations(recompile, read_config, upgrade=True, upgrade_packages=packages) @cli.command() @click.pass_context def verify(ctx): """Verify environments.""" oks = run_configurations( skipper(verify_environments), read_sections, ) ctx.exit(0 if False not in oks else 1) def skipper(func): """Decorator that memorizes base_dir, in_ext and out_ext from OPTIONS and skips execution for duplicates. """ @functools.wraps(func) def wrapped(): """Dummy docstring to make pylint happy.""" key = (OPTIONS['directory'], OPTIONS['in_ext'], OPTIONS['out_ext']) if key not in seen: seen[key] = func() return seen[key] seen = {} return wrapped def run_configurations(callback, sections_reader, **overrides): """Parse configurations and execute callback for matching. Args: callback: Function to execute for each matching section sections_reader: Function that returns configuration sections """ sections = sections_reader() if sections is None: logger.info("Configuration not found in pyproject.toml " "or one of .ini files. Running with default settings") recompile() return [] elif sections == []: logger.info("Configuration does not match current runtime. " "Exiting") results = [] for section, options in sections: OPTIONS.clear() OPTIONS.update(DEFAULT_OPTIONS) OPTIONS.update(options) OPTIONS.update(overrides) logger.debug("Running configuration from section \"%s\". OPTIONS: %r", section, OPTIONS) results.append(callback()) return results if __name__ == '__main__': cli() # pylint: disable=no-value-for-parameter ================================================ FILE: pipcompilemulti/config.py ================================================ """Get tasks options from INI file""" import sys import os import configparser from typing import Dict, List, Union from functools import lru_cache from .features.base import BaseFeature, ClickOption def read_config(): """Read requirements.ini and return list of pairs (name, options) If no requirements sections found, return None. If some sections found, but none matches current runtime, return empty list. """ return filter_sections(read_sections()) def filter_sections(sections): """Filter through pairs (name, options) leaving only those that match runtime. If no requirements sections found, return None. If some sections found, but none matches current runtime, return empty list. """ if not sections: return None jobs = [] matchers = python_version_matchers() for name, options in sections: target_version = options.pop('python', None) if target_version in matchers: jobs.append((name, options)) return jobs def read_sections(): """Read ini/toml files and return list of pairs (name, options)""" sections = [] sections.extend(_read_toml_sections()) sections.extend(_read_cfg_sections()) return sections def _read_cfg_sections(): parser = configparser.ConfigParser() parser.read(('requirements.ini', 'setup.cfg', 'tox.ini')) return [ ( name, { key: parse_value(key, parser[name][key]) for key in parser[name] } ) for name in parser.sections() if 'requirements' in name ] def _read_toml_sections(): # pylint: disable=import-outside-toplevel try: import tomllib except ImportError: try: import tomli as tomllib except ImportError: return [] if not os.path.isfile("pyproject.toml"): return [] with open("pyproject.toml", mode="rb") as fp: toml_config = tomllib.load(fp) config = toml_config.get("tool", {}).get("requirements") if not config: return [] if not all(isinstance(v, dict) for v in config.values()): # Allow [tool.requirements] section config = {'config': config} return [ ( name, { key: parse_value(key, _make_toml_scalar(value)) for key, value in section.items() } ) for name, section in config.items() ] def _make_toml_scalar(v: object) -> str: """Coalesce TOML value to string. TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), however we need to convert all scalar values to str for compatibility with the rest of the configuration system, which expects strings only. """ return ",".join(v) if isinstance(v, list) else str(v) @lru_cache(maxsize=None) def _collect_feature_options() -> Dict[str, ClickOption]: subclasses = BaseFeature.__subclasses__() for feature_class in BaseFeature.__subclasses__(): subclasses.extend(feature_class.__subclasses__()) return { feature_class.OPTION_NAME: feature_class.CLICK_OPTION for feature_class in subclasses if feature_class.OPTION_NAME and feature_class.CLICK_OPTION } def parse_value(key: str, value: str) -> Union[str, List[str], bool]: """Parse value according to the option definition (bool or list)""" options = _collect_feature_options() click_option = options.get(key) if click_option: if click_option.multiple: return [item.strip() for item in value.split(',')] if click_option.is_flag: return value.lower() not in ('false', 'off', 'no') return value def python_version_matchers(): """Return set of string representations of current python version""" version = sys.version_info patterns = [ "{0}", "{0}{1}", "{0}.{1}", ] matchers = [ pattern.format(*version) for pattern in patterns ] + [None] return set(matchers) ================================================ FILE: pipcompilemulti/deduplicate.py ================================================ """Remove packages included in referenced environments.""" import logging from pipcompilemulti.utils import recursive_refs, merged_packages logger = logging.getLogger("pip-compile-multi") class PackageDeduplicator: """Remove packages included in referenced environments.""" def __init__(self): self.env_packages = {} self.env_confs = None def on_discover(self, env_confs): """Save environment references.""" self.env_confs = env_confs def register_packages_for_env(self, in_path, packages): """Save environment packages.""" self.env_packages[in_path] = packages def ignored_packages(self, in_path): """Get package mapping from name to version for referenced environments.""" if self.env_confs is None: return {} rrefs = recursive_refs(self.env_confs, in_path) return IgnoredPackages(merged_packages(self.env_packages, rrefs)) def recursive_refs(self, in_path): """Return recursive list of environment names referenced by in_path.""" if self.env_confs is None: return {} return recursive_refs(self.env_confs, in_path) class IgnoredPackages: """Mapping from package name to version. Handles name normalization for packages like: zope.interface, zope-interface, zope_interface. """ _DELIMITERS = ('_', '-', '.') def __init__(self, package_versions): self._package_versions = package_versions self._stems = { self._make_stem(name): name for name in self._package_versions } def __getitem__(self, key): canonical_key = self._stems[self._make_stem(key)] return self._package_versions[canonical_key] def __contains__(self, key): return self._make_stem(key) in self._stems @classmethod def _make_stem(cls, name): for delim in cls._DELIMITERS: name = name.replace(delim, '-') return name.lower() ================================================ FILE: pipcompilemulti/dependency.py ================================================ """Dependency class""" import re from .features import FEATURES class Dependency(object): # pylint: disable=too-many-instance-attributes r"""Single dependency. Comment may span multiple lines. >>> print(Dependency( ... "six==1.0\n " ... " --hash=abcdef\n" ... " # via\n" ... " # app\n" ... " # pkg" ... ).serialize()) six==1.0 \ --hash=abcdef # via # app # pkg >>> print(Dependency( ... "six==1.0\n" ... " # via\n" ... " # app\n" ... " # pkg" ... ).serialize()) six==1.0 # via # app # pkg >>> # Old-style one-line >>> print(Dependency("six==1.0 # via pkg").serialize()) six==1.0 # via pkg >>> print(Dependency("-e https://site#egg=pkg==1\n # via lib").serialize()) https://site#egg=pkg==1 # via lib >>> print(Dependency('dep==1 ; sys_platform == "darwin"').serialize()) dep==1 ; sys_platform == "darwin" """ COMMENT_JUSTIFICATION = 26 # Example: # unidecode==0.4.21 # via myapp # [package] [version] [comment] RE_DEPENDENCY = re.compile( r'(?iu)(?P\S+)' r'==' r'(?P\S+)' r'(?P\s+;\s+.+?)?' r'(?P(?:\s*--hash=\S+)+)?' r'(?P(?:\s*#.*)+)?$' ) RE_EDITABLE_FLAG = re.compile( r'^-e ' ) # -e git+https://github.com/ansible/docutils.git@master#egg=docutils # -e "git+https://github.com/zulip/python-zulip-api.git@ # 0.4.1#egg=zulip==0.4.1_git&subdirectory=zulip" RE_VCS_DEPENDENCY = re.compile( r'(?iu)(?P-e)?' r'\s*' r'(?P\S+#egg=)' r'(?P[a-z0-9-_.]+)' r'(?P\S*)' r'(?P\s+;\s+.+?)?' r'(?P(?:\s*#.*)+)?$' ) # New format, replacing old VCS dependency: # docutils @ git+https://github.com/ansible/docutils.git@master RE_AT_DEPENDENCY = re.compile( r'(?iu)(?P-e)?' r'\s*' r'(?P[a-z0-9-_.]+)' r' @ ' r'(?P\S+)' r'(?P\s+;\s+.+?)?' r'(?P(?:\s*#.*)+)?$' ) # Example: 2022.02.1 -> 2022.2.1 RE_IGNORED_ZEROS = re.compile(r"(?<=\.)0+(?=\d)") def __init__(self, line): self.is_vcs = False self.is_at = False self.valid = True self.line = line vcs = self.RE_VCS_DEPENDENCY.match(line) if vcs: self.is_vcs = True self.package = vcs.group('package') self.version = '' self.markers = vcs.group('markers') or '' self.hashes = '' # No way! self.comment = (vcs.group('comment') or '').rstrip() self.comment_span = self._adjust_span(vcs.span('comment'), vcs) return at_url = self.RE_AT_DEPENDENCY.match(line) if at_url: self.is_at = True self.package = at_url.group('package') self.version = '' self.markers = at_url.group('markers') or '' self.hashes = '' # ??? self.comment = (at_url.group('comment') or '').rstrip() self.comment_span = self._adjust_span(at_url.span('comment'), at_url) return regular = self.RE_DEPENDENCY.match(line) if regular: self.package = regular.group('package') self.version = self.RE_IGNORED_ZEROS.sub("", regular.group('version').strip()) self.markers = regular.group('markers') or '' self.hashes = (regular.group('hashes') or '').strip() self.comment = (regular.group('comment') or '').rstrip() self.comment_span = self._adjust_span(regular.span('comment'), regular) return self.valid = False def serialize(self): """ Render dependency back in string using: ~= if package is internal == otherwise """ if self.is_vcs or self.is_at: return "{}{}".format( self.without_editable(self.line[:self.comment_span[0]]).strip(), FEATURES.process_dependency_comments(self.comment), ) equal = FEATURES.constraint(self.package) package_version = '{package}{equal}{version} '.format( package=self.without_editable(self.package), version=self.version, equal=equal, ) if self.markers: package_version = '{}{} '.format(package_version.strip(), self.markers) if self.hashes: hashes = self.hashes.split() lines = [package_version.strip()] lines.extend(hashes) result = ' \\\n '.join(lines) result += FEATURES.process_dependency_comments(self.comment) return result else: if self.comment.startswith('\n'): return ( package_version.rstrip() + FEATURES.process_dependency_comments(self.comment).rstrip() ) return '{0}{1}'.format( package_version.ljust(self.COMMENT_JUSTIFICATION), self.comment.lstrip(), ).rstrip() # rstrip for empty comment @classmethod def without_editable(cls, line): """ Remove the editable flag. It's there because pip-compile can't yet do without it (see https://github.com/jazzband/pip-tools/issues/272 upstream), but in the output of pip-compile it's no longer needed. """ if 'git+git@' in line: # git+git can't be installed without -e: return line return cls.RE_EDITABLE_FLAG.sub('', line) def drop_post(self, in_path): """Remove .postXXXX postfix from version if needed.""" self.version = FEATURES.drop_post(in_path, self.package, self.version) @staticmethod def _adjust_span(span, matchobj): if span == (-1, -1): length = matchobj.span()[1] return (length, length) return span ================================================ FILE: pipcompilemulti/discover.py ================================================ """Environment discovery""" import os import glob from collections import deque from toposort import toposort_flatten from .environment import Environment from .utils import fix_reference_path, extract_env_name __all__ = ('discover',) def discover(glob_pattern): """ Find all files matching given glob_pattern, parse them, and return list of environments: Recursively follow referenced files not matched by glob_pattern. >>> import os >>> envs = discover(os.path.join('requirements', '*.in')) >>> # import pprint; pprint.pprint(envs) >>> envs == [ ... {'in_path': os.path.join('requirements', 'base.in'), 'name': 'base', ... 'refs': set()}, ... {'in_path': os.path.join('requirements', 'test.in'), 'name': 'test', ... 'refs': {'base.in'}}, ... {'in_path': os.path.join('requirements', 'local.in'), 'name': 'local', ... 'refs': {'test.in'}}, ... {'in_path': os.path.join('requirements', 'testwin.in'), 'name': 'testwin', ... 'refs': {'test.in'}} ... ] True """ to_visit = deque(map(os.path.normpath, glob.glob(glob_pattern))) envs, all_in_paths = {}, set() while to_visit: in_path = to_visit.pop() # name = if in_path in all_in_paths: continue all_in_paths.add(in_path) envs[in_path] = { 'in_path': in_path, 'name': extract_env_name(in_path), 'refs': Environment.parse_references(in_path), } for ref in envs[in_path]['refs']: to_visit.append(fix_reference_path( orig_path=in_path, ref_path=ref )) return order_by_refs(envs.values()) def order_by_refs(envs): """Return topologicaly sorted list of environments. I.e. all referenced environments are placed before their references. """ topology = { env['in_path']: set(fix_reference_path(env['in_path'], ref) for ref in env['refs']) for env in envs } by_in_path = { env['in_path']: env for env in envs } return [ by_in_path[in_path] for in_path in toposort_flatten(topology) ] ================================================ FILE: pipcompilemulti/environment.py ================================================ """Environment class""" import os import re import sys import logging import subprocess from .dependency import Dependency from .features import FEATURES from .deduplicate import PackageDeduplicator from .utils import extract_env_name logger = logging.getLogger("pip-compile-multi") class Environment(object): """requirements file""" RE_REF = re.compile(r'^(?:-r|--requirement)\s*(?P\S+).*$') RE_COMMENT = re.compile(r'^\s*#.*$') def __init__(self, in_path, deduplicator=None): """ Args: in_path: relative path to input file, e.g. requirements/base.in """ self.in_path = in_path self._dedup = deduplicator or PackageDeduplicator() self.ignore = self._dedup.ignored_packages(in_path) self.packages = {} self._outfile_pkg_names = None def maybe_create_lockfile(self): """ Write recursive dependencies list to outfile unless the goal is to upgrade specific package(s) which don't already appear. Populate package ignore set in either case and return boolean indicating whether outfile was written. """ logger.info( "Locking %s to %s. References: %r", self.infile, self.outfile, sorted(self._dedup.recursive_refs(self.in_path)), ) if not FEATURES.affected(self.in_path): self.fix_lockfile() # populate ignore set return False self.create_lockfile() return True def create_lockfile(self): """ Write recursive dependencies list to outfile with hard-pinned versions. Then fix it. """ original_in_file = "" sink_out_path = FEATURES.sink_out_path() try: if sink_out_path and sink_out_path != self.outfile: original_in_file = self._read_infile() self._inject_sink() with subprocess.Popen(self.pin_command, **FEATURES.pipe_arguments()) as process: stdout, stderr = process.communicate() finally: if original_in_file: self._restore_in_file(original_in_file) if process.returncode == 0: self.fix_lockfile() else: logger.critical("ERROR executing %s", ' '.join(self.pin_command)) logger.critical("Exit code: %s", process.returncode) if stdout: logger.critical(stdout.decode('utf-8')) if stderr: logger.critical(stderr.decode('utf-8')) raise RuntimeError("Failed to pip-compile {0}".format(self.infile)) @classmethod def parse_references(cls, filename): """ Read filename line by line searching for pattern: -r file.in or --requirement file.in return set of matched file names. E.g. {'file1.in', 'file2.in'} """ references = set() with open(filename, encoding="utf-8") as fobj: for line in fobj: matched = cls.RE_REF.match(line) if matched: references.add(matched.group('path')) return references @property def name(self): """Generate environment name from in_path.""" return extract_env_name(self.in_path) @property def infile(self): """Path of the input file""" return self.in_path @property def outfile(self): """Path of the output file""" return FEATURES.compose_output_file_path(self.in_path) @property def pin_command(self): """Compose dependency resolution command based on selected tool""" # Use the same interpreter binary parts = [sys.executable or 'python', '-m'] parts.extend(FEATURES.pin_command()) parts.extend(FEATURES.pin_options(self.in_path)) parts.extend(['--output-file', self.outfile, self.infile]) return parts def fix_lockfile(self): """Run each section of outfile through fix_pin""" with open(self.outfile, 'rt', encoding="utf-8") as fp: sections = [ self.fix_pin(section) for section in self.parse_sections(self.concatenated(fp)) ] with open(self.outfile, 'wt', encoding="utf-8") as fp: fp.writelines([ section + '\n' for section in sections if section is not None ]) self._dedup.register_packages_for_env(self.in_path, self.packages) @staticmethod def concatenated(fp): r"""Read lines from fp concatenating on backslash (\\) >>> env = Environment('') >>> list(env.concatenated([ ... 'pkg', 'pkg # comment', 'pkg', '# comment', '# one more', ... 'foo', ' # via', '', ' # pkg', ... ])) ['pkg', 'pkg # comment', 'pkg', '# comment', '# one more', 'foo', ' # via', '', ' # pkg'] """ line_parts = [] for line in fp: line = line.rstrip() if line.endswith('\\'): line_parts.append(line[:-1].rstrip()) else: line_parts.append(line) yield ' '.join(line_parts) line_parts[:] = [] if line_parts: # Impossible: raise RuntimeError("Compiled file ends with backslash \\") def parse_sections(self, lines): r"""Combine lines with following comments into sections. >>> env = Environment('') >>> list(env.parse_sections([ ... 'pkg', 'pkg # comment', 'pkg', '# comment', '# one more', ... 'foo', ' # via', '', ' # pkg', ... ])) ['pkg', 'pkg # comment', 'pkg\n# comment\n# one more', 'foo\n # via', '\n # pkg'] """ section = [] for line in lines: if self.RE_COMMENT.match(line): section.append(line) else: if section: yield '\n'.join(section) section = [line] if section: yield '\n'.join(section) def fix_pin(self, section): """ Fix dependency by removing post-releases from versions and loosing constraints on internal packages. Drop packages from ignore set Also populate packages set """ dep = Dependency(section) if dep.valid: if dep.package in self.ignore: ignored_version = self.ignore[dep.package] if ignored_version is not None: # ignored_version can be None to disable conflict detection if dep.version and dep.version != ignored_version: logger.error( "Package %s was resolved to different " "versions in different environments: %s and %s", dep.package, dep.version, ignored_version, ) raise RuntimeError( "Please add constraints for the package " "version listed above" ) return None self.packages[dep.package] = dep.version dep.drop_post(self.in_path) return dep.serialize() return section.rstrip() def add_references(self, other_in_paths): """Add references to other_in_paths in outfile""" if not other_in_paths: # Skip on empty list return with open(self.outfile, 'rt', encoding="utf-8") as fp: header, body = self.split_header(fp) with open(self.outfile, 'wt', encoding="utf-8") as fp: fp.writelines(header) fp.writelines( '-r {0}\n'.format( FEATURES.compose_output_file_path(other_in_path) ) for other_in_path in sorted(other_in_paths) ) fp.writelines(body) @staticmethod def split_header(fp): """ Read file pointer and return pair of lines lists: first - header, second - the rest. """ body_start, header_ended = 0, False lines = [] for line in fp: if line.startswith('#') and not header_ended: # Header text body_start += 1 else: header_ended = True lines.append(line) return lines[:body_start], lines[body_start:] def replace_header(self, header_text): """Replace pip-compile header with custom text""" with open(self.outfile, 'rt', encoding="utf-8") as fp: _, body = self.split_header(fp) with open(self.outfile, 'wt', encoding="utf-8") as fp: fp.write(header_text) fp.writelines(body) def _read_infile(self): with open(self.infile, "rt", encoding="utf-8") as fp: return fp.read() def _restore_in_file(self, content): with open(self.infile, "wt", encoding="utf-8") as fp: return fp.write(content) def _inject_sink(self): rel_sink_out_path = os.path.normpath(os.path.relpath( FEATURES.sink_out_path(), os.path.dirname(self.infile), )) with open(self.infile, "at", encoding="utf-8") as fp: return fp.write("\n\n-c {}\n".format(rel_sink_out_path)) ================================================ FILE: pipcompilemulti/features/__init__.py ================================================ """Features as modules.""" from .controller import FeaturesController FEATURES = FeaturesController() ================================================ FILE: pipcompilemulti/features/add_hashes.py ================================================ """ Generate hashes =============== Put package hash after pinned version for additional security. Format for this option is .. code-block:: text -g, --generate-hashes TEXT Input file name (base.in, requirements/test.in, etc) that needs packages hashes. Can be supplied multiple times. For backwards compatibility can be short environment name (base, test, etc.) In configuration file, use ``generate_hashes`` option with comma-separated list of paths:: [requirements] generate_hashes = requirements/base.txt, requirements/docs.txt Example invocation: .. code-block:: shell $ pip-compile-multi -g requirements/base.txt -g requirements/docs.txt Example output: .. code-block:: text pip-tools==1.11.0 \ --hash=sha256:50288eb066ce66dbef5401a21530712a93c659fe480c7d8d34e2379300555fa1 \ --hash=sha256:ba427b68443466c389e3b0b0ef55f537ab39344190ea980dfebb333d0e6a50a3 first==2.0.1 \ --hash=sha256:3bb3de3582cb27071cfb514f00ed784dc444b7f96dc21e140de65fe00585c95e \ --hash=sha256:41d5b64e70507d0c3ca742d68010a76060eea8a3d863e9b5130ab11a4a91aa0e \ # via pip-tools ``pip`` requires all packages to have hashes if at least one has it. ``pip-compile-multi`` will recursively propagate this option to all environments that are referencing or referenced by selected environments. """ # noqa: E501 import os from pipcompilemulti.utils import reference_cluster from .base import BaseFeature, ClickOption class AddHashes(BaseFeature): """Write hashes for pinned packages.""" OPTION_NAME = 'generate_hashes' CLICK_OPTION = ClickOption( long_option='--generate-hashes', short_option='-g', multiple=True, help_text='Input file name (base.in, requirements/test.in, etc) ' 'that needs packages hashes. ' 'Can be supplied multiple times.', ) def __init__(self, controller): self._controller = controller self._hashed_by_reference = None @property def enabled_in_paths(self): """Convert list of .in paths to a set. For backwards compatibility, check if passed value is env name and convert it to in_path. """ names_or_paths = self.value or [] in_paths = set() for name_or_path in names_or_paths: in_path = self._controller.compose_input_file_path(name_or_path) if os.path.exists(in_path): in_paths.add(in_path) else: in_paths.add(name_or_path) return in_paths def on_discover(self, env_confs): """Save environment names that need hashing.""" self._hashed_by_reference = set() for in_path in self.enabled_in_paths: self._hashed_by_reference.update( reference_cluster(env_confs, in_path) ) def _needs_hashes(self, in_path): assert self._hashed_by_reference is not None return in_path in self._hashed_by_reference def pin_options(self, in_path): """Return --generate-hashes if env requires it.""" if self._needs_hashes(in_path): return ['--generate-hashes'] return [] ================================================ FILE: pipcompilemulti/features/annotate_index.py ================================================ """ .. _annotate_index: Add index URL annotation ======================== Control addition of ``--index-url`` options to the output files. Corresponds to ``pip-compile``'s ``--emit-index-url / --no-emit-index-url`` flag. The URL can be defined in the input file as in this example: .. code-block:: text --index-url=https://pypi.tuna.tsinghua.edu.cn/simple/ six click This option is similar to `extra_index_url`_. The difference is that URL is saved in the output files, which means that it doesn't need to be configured in every environment. .. code-block:: text --annotate-index / --no-annotate-index Add index URL to generated files (default false) In configuration file, use ``annotate_index`` option:: [requirements] annotate_index = True Note: the default behavior is not to add the index, i.e., ``--no-annotate-index``. """ from .base import ClickOption from .forward import ForwardOption class AnnotateIndex(ForwardOption): """Optionally annotate the index URL to the generated files.""" OPTION_NAME = "annotate_index" CLICK_OPTION = ClickOption( long_option="--annotate-index/--no-annotate-index", default=False, is_flag=True, help_text="Add the index URL to generated files (default false).", ) enabled_pin_options = ["--emit-index-url"] disabled_pin_options = ["--no-emit-index-url"] ================================================ FILE: pipcompilemulti/features/autoresolve.py ================================================ """ .. _autoresolve: Autoresolve cross-file conflicts ================================ Compile requirements file, that references all other files first, and than use it as a constraint. .. code-block:: text --autoresolve/--no-autoresolve Automatically resolve cross-file conflicts. When using ``requirements`` command, this option is enabled by default. To disable it, set it to ``False`` in a configuration file:: [requirements] autoresolve = False This strategy allows to resolve cross-file conflicts of two types: 1. Files FOO and BAR both have dependency PKG, but compile it to different versions. 2. FOO has PKG resolved to version 3. BAR references FOO, but also has another dependency, that constraints PKG to version 2. By compiling all-including file (aka *sink*), that references both FOO and BAR first, pip-compile-multi generates conflict-free set of versions. After that, this compiled file is passed as a constraint for compiling all requirements files. As the last step, the *sink* file is compiled again preserving reference files and skipping duplicate packages. .. note:: This feature works only if your project has a single requirements file, that references (directly or indirectly) all other files. """ from pipcompilemulti.utils import recursive_refs from .base import BaseFeature, ClickOption class Autoresolve(BaseFeature): """Detect sink file and use it unless the feature is explicitly disabled.""" OPTION_NAME = 'autoresolve' CLICK_OPTION = ClickOption( long_option='--autoresolve/--no-autoresolve', is_flag=True, default=False, help_text='Automatically resolve cross-file conflicts.', ) def __init__(self): self._sink_path = None @property def enabled(self): """Whether feature was explicitly disabled or not.""" return self.value def on_discover(self, env_confs): """Save set of all (recursive) included environments.""" self._sink_path = self._find_sink(env_confs) def sink_path(self): """Return sink path if it's enabled. Otherwise None""" return self._sink_path if self.enabled else None @staticmethod def _find_sink(envs): """Try to find requirements sink. Sink is a requirements file that references all other requirement files. If no sink exists, return None. >>> Autoresolve._find_sink([ ... {'in_path': 'base', 'refs': set()}, ... {'in_path': 'test', 'refs': {'base'}}, ... {'in_path': 'local', 'refs': {'test', 'base'}}, ... ]) 'local' >>> Autoresolve._find_sink([ ... {'in_path': 'base', 'refs': set()}, ... {'in_path': 'test', 'refs': {'base'}}, ... {'in_path': 'doc', 'refs': set()}, ... ]) >>> Autoresolve._find_sink([ ... {'in_path': 'base', 'refs': set()}, ... {'in_path': 'foo', 'refs': {'base'}}, ... {'in_path': 'bar', 'refs': {'base'}}, ... {'in_path': 'all', 'refs': {'foo', 'bar'}}, ... ]) 'all' """ all_envs = {env['in_path'] for env in envs} for env in envs: included_envs = set(recursive_refs(envs, env['in_path'])) | {env['in_path']} if all_envs == included_envs: return env['in_path'] return None ================================================ FILE: pipcompilemulti/features/backtracking.py ================================================ """ Backtracking resolver ===================== Pip has an option to enable `backtracking`_ conflict resolution logic, which can automatically downgrade some dependencies to meet constraints from other packages. See also the `a note on resolvers`_ in ``pip-compile`` project. .. code-block:: text --backtracking / --no-backtracking Enable backtracking resolver. Translates to pip-compile --resolver=backtracking option. In configuration file, use ``backtracking`` option:: [requirements] backtracking = True .. _backtracking: https://pip.pypa.io/en/latest/user_guide/\ #changes-to-the-pip-dependency-resolver-in-20-3-2020 .. _a note on resolvers: https://github.com/jazzband/pip-tools#a-note-on-resolvers """ from .base import ClickOption from .forward import ForwardOption class Backtracking(ForwardOption): """Enable backtracking resolver.""" OPTION_NAME = 'backtracking' CLICK_OPTION = ClickOption( long_option='--backtracking/--no-backtracking', is_flag=True, default=False, help_text='Enable backtracking resolver. Translates to ' 'pip-compile --resolver=backtracking option.' ) enabled_pin_options = ['--resolver=backtracking'] disabled_pin_options = ['--resolver=legacy'] ================================================ FILE: pipcompilemulti/features/base.py ================================================ """Common functionality for features activated by command line option.""" import click from ..options import OPTIONS class ClickOption: """Click option parameters.""" def __init__(self, long_option='', short_option='', *, is_flag=False, default=None, multiple=False, help_text=''): self.long_option = long_option self.short_option = short_option self.is_flag = is_flag self.default = default self.multiple = multiple self.help_text = help_text def decorate(self, command): """Decorate click command with this option.""" return self.decorator()(command) def decorator(self): """Create click command decorator with this option.""" args = [self.long_option] kwargs = { "is_flag": self.is_flag, "multiple": self.multiple, "help": self.help_text, } if self.short_option: args.append(self.short_option) if self.default: kwargs.update(default=self.default) return click.option(*args, **kwargs) @property def argument_name(self): """Generate command argument name from long option. >>> ClickOption("--param-name").argument_name 'param_name' >>> ClickOption("--param-name/--no-param-name").argument_name 'param_name' """ return self.long_option.lstrip('--').split('/', 1)[0].replace('-', '_') class BaseFeature: """Base class for features.""" OPTION_NAME = None CLICK_OPTION = None def bind(self, command): """Bind feature's click option to passed command.""" return self.CLICK_OPTION.decorate(command) def extract_option(self, kwargs): """Pop option value from kwargs and save it in OPTIONS. If option was saved before and new value is the same as default, then keep previous value. This allows passing options both before and after ``verify`` command. """ new_value = kwargs.pop(self.CLICK_OPTION.argument_name) if self.OPTION_NAME in OPTIONS and new_value == self.CLICK_OPTION.default: # Do not overwrite with default if already set. return OPTIONS[self.OPTION_NAME] = new_value @property def value(self): """Option value.""" return OPTIONS.get(self.OPTION_NAME, self.CLICK_OPTION.default) @value.setter def value(self, new_value): OPTIONS[self.OPTION_NAME] = new_value ================================================ FILE: pipcompilemulti/features/base_dir.py ================================================ """ Requirements Directory ====================== While it's a common practice to put requirements files inside ``requirements`` directory, it's not always the case. The directory can be overridden with this option: .. code-block:: text -d, --directory TEXT Directory path with requirements files In configuration file, use ``directory`` option. For example, to use project root, specify:: [requirements] directory = . """ import os from .base import BaseFeature, ClickOption class BaseDir(BaseFeature): """Override input file extension.""" OPTION_NAME = 'directory' CLICK_OPTION = ClickOption( long_option='--directory', short_option='-d', default="requirements", is_flag=False, help_text='Directory path with requirements files.', ) @property def path(self): """Get the base directory path. >>> BaseDir().path == 'requirements' True """ return self.value def file_path(self, file_name): """Compose file path for a given file name. >>> import os.path >>> expected = os.path.join('requirements', 'base.txt') >>> expected == BaseDir().file_path('base.txt') True """ return os.path.join(self.value, file_name) ================================================ FILE: pipcompilemulti/features/build_isolation.py ================================================ """ Build isolation =============== Allows disabling build isolation through the equivalent ``pip-compile`` flag. Build isolation is enabled by default. .. code-block:: text --build-isolation / --no-build-isolation Enable isolation when building a modern source distribution. Build dependencies specified by PEP 518 must be already installed if build isolation is disabled. In configuration file, use ``build_isolation`` option:: [requirements] build_isolation = False """ from .base import ClickOption from .forward import ForwardOption class BuildIsolation(ForwardOption): """Proxies build isolation flag to pip-compile""" OPTION_NAME = 'build_isolation' CLICK_OPTION = ClickOption( long_option='--build-isolation/--no-build-isolation', is_flag=True, default=True, help_text=( 'Enable isolation when building a modern source distribution. ' 'Build dependencies specified by PEP 518 must be already ' 'installed if build isolation is disabled.' ), ) disabled_pin_options = ['--no-build-isolation'] ================================================ FILE: pipcompilemulti/features/compatible.py ================================================ """ Compatible Releases =================== `PEP-440`_ describes compatible release operator ``~=``. Sometimes it's useful to have some of the dependencies pinned using this operator. For example, rapidly changing internal libraries. The format for this option is:: -c, --compatible TEXT where TEXT is a `glob`_ pattern for library name. This option can be supplied multiple times. In configuration file, use ``compatible`` option with comma-separated list of package names:: [requirements] compatible = package1, package2 .. _glob: https://en.wikipedia.org/wiki/Glob_(programming) .. _PEP-440: https://www.python.org/dev/peps/pep-0440/#compatible-release """ from fnmatch import fnmatch from .base import BaseFeature, ClickOption class Compatible(BaseFeature): """Use ~= for selected packages.""" OPTION_NAME = 'compatible_patterns' CLICK_OPTION = ClickOption( long_option='--compatible', short_option='-c', multiple=True, help_text='Glob expression for packages with compatible (~=) ' 'version constraint. Can be supplied multiple times.' ) @property def patterns(self): """Use empty list as the default.""" return self.value or [] def constraint(self, package_name): """Return ``~=`` if package_name matches patterns, ``==`` otherwise. >>> from pipcompilemulti.options import OPTIONS >>> feature = Compatible() >>> OPTIONS[feature.OPTION_NAME] = ['xxx'] >>> feature.constraint('package') '==' >>> feature.constraint('xxx') '~=' """ return '~=' if self.is_matched(package_name) else '==' def is_matched(self, package_name): """Whether package name matches one of configured glob patterns.""" package = package_name.lower() for pattern in self.patterns: if fnmatch(package, pattern): return True return False ================================================ FILE: pipcompilemulti/features/controller.py ================================================ """Aggregate all features in a single controller.""" import os from functools import wraps from .add_hashes import AddHashes from .annotate_index import AnnotateIndex from .autoresolve import Autoresolve from .backtracking import Backtracking from .base_dir import BaseDir from .build_isolation import BuildIsolation from .compatible import Compatible from .emit_find_links import EmitFindLinks from .emit_trusted_host import EmitTrustedHost from .extra_index_url import ExtraIndexUrl from .file_extensions import InputExtension, OutputExtension from .forbid_post import ForbidPost from .header import CustomHeader from .limit_in_paths import LimitInPaths from .live_output import LiveOutput from .skip_constraint_comments import SkipConstraintComments from .strip_extras import StripExtras from .unsafe import AllowUnsafe from .upgrade import UpgradeAll, UpgradeSelected from .use_cache import UseCache from .use_uv import UseUV class FeaturesController: """Gateway to a list of features.""" # pylint: disable=too-many-instance-attributes def __init__(self): self.add_hashes = AddHashes(self) self.allow_unsafe = AllowUnsafe() self.annotate_index = AnnotateIndex() self.autoresolve = Autoresolve() self.backtracking = Backtracking() self.base_dir = BaseDir() self.build_isolation = BuildIsolation() self.compatible = Compatible() self.emit_find_links = EmitFindLinks() self.emit_trusted_host = EmitTrustedHost() self.extra_index_url = ExtraIndexUrl() self.forbid_post = ForbidPost() self.header = CustomHeader() self.input_extension = InputExtension() self.limit_in_paths = LimitInPaths() self.live_output = LiveOutput() self.output_extension = OutputExtension() self.skip_constraint_comments = SkipConstraintComments() self.strip_extras = StripExtras() self.upgrade_all = UpgradeAll(self) self.upgrade_selected = UpgradeSelected(self) self.use_cache = UseCache() self.use_uv = UseUV() self._features = [ self.add_hashes, self.allow_unsafe, self.annotate_index, self.autoresolve, self.backtracking, self.base_dir, self.build_isolation, self.compatible, self.emit_find_links, self.emit_trusted_host, self.extra_index_url, self.forbid_post, self.header, self.input_extension, self.limit_in_paths, self.live_output, self.output_extension, self.skip_constraint_comments, self.strip_extras, self.upgrade_all, self.upgrade_selected, self.use_cache, self.use_uv, ] def bind(self, command): """Bind all features to click command.""" @wraps(command) def save_command_options(*args, **kwargs): """Save option values and call original command without it.""" for feature in self._features: feature.extract_option(kwargs) return command(*args, **kwargs) for feature in self._features: save_command_options = feature.bind(save_command_options) return save_command_options def pin_command(self): """Return list of pin command parameters.""" if self.use_uv.value: if not self.use_uv.is_available(): raise RuntimeError( "UV package is not available. " "Please install it with: pip install uv" ) return [ 'uv', 'pip', 'compile', '--no-header', ] return [ 'piptools', 'compile', '--no-header', '--verbose', ] def pin_options(self, in_path): """Return list of options to pin command.""" options = [] options.extend(self.add_hashes.pin_options(in_path)) options.extend(self.annotate_index.pin_options()) options.extend(self.build_isolation.pin_options()) options.extend(self.emit_find_links.pin_options()) options.extend(self.extra_index_url.pin_options()) options.extend(self.upgrade_all.pin_options()) options.extend(self.upgrade_selected.pin_options()) if not self.use_uv.value: options.extend(self.allow_unsafe.pin_options()) options.extend(self.emit_trusted_host.pin_options()) options.extend(self.backtracking.pin_options()) options.extend(self.use_cache.pin_options()) options.extend(self.strip_extras.pin_options()) return options def compose_input_file_path(self, basename): """Return input file path by environment name.""" return self.base_dir.file_path( self.input_extension.compose_input_file_name(basename) ) def compose_output_file_path(self, in_path): """Return output file path by environment name.""" return self.output_extension.compose_output_file_path(in_path) def drop_post(self, in_path, package_name, version): """Whether post versions are forbidden for passed environment name.""" if self.forbid_post.post_forbidden(in_path): return self.forbid_post.drop_post(version) if self.compatible.is_matched(package_name): return self.forbid_post.drop_post(version) return version def constraint(self, package_name): """Return ``~=`` if package_name matches patterns, ``==`` otherwise.""" return self.compatible.constraint(package_name) def on_discover(self, env_confs): """Configure features with a list of discovered environments. Returns a new possibly shorter env list. """ self.upgrade_selected.reset() self.limit_in_paths.on_discover(env_confs) limited_env_confs = [env for env in env_confs if self.included(env['in_path'])] self.add_hashes.on_discover(limited_env_confs) self.autoresolve.on_discover(limited_env_confs) return limited_env_confs def affected(self, in_path): """Whether environment is affected by upgrade command.""" if self.upgrade_all.enabled: return True if self.upgrade_selected.affected(in_path): return True return in_path == self.autoresolve.sink_path() def included(self, in_path): """Whether in_path is included directly or by reference.""" return self.limit_in_paths.included(in_path) def get_header_text(self): """Text to put in the beginning of each generated file.""" return self.header.text def sink_in_path(self): """Return input sink path if it's enabled. Otherwise None""" return self.autoresolve.sink_path() def sink_out_path(self): """Return sink output path if it's enabled and exists. Otherwise None""" infile = self.autoresolve.sink_path() if not infile: return None outfile = self.compose_output_file_path(infile) if not os.path.exists(outfile): return None return outfile def process_dependency_comments(self, comment): """Process comments of locked dependency (e.g. # via xxx).""" return self.skip_constraint_comments.process_dependency_comments(comment) def pipe_arguments(self): """Values for stdout and stderr arguments to subprocess.Popen.""" return self.live_output.pipe_arguments() ================================================ FILE: pipcompilemulti/features/emit_find_links.py ================================================ """ .. _emit_find_links: Add find-links annotation ========================= Control addition of ``--find-links`` options to the output files. Corresponds to ``pip-compile``'s and ``uv pip compile``'s ``--emit-find-links / --no-emit-find-links`` flag. The URL can be defined in the input file as in this example: .. code-block:: text --find-links=https://download.pytorch.org/whl/torch_stable.html torch By default, ``--find-links`` entries from input files are preserved in the generated output files. Pass ``--no-emit-find-links`` to strip them. .. code-block:: text --emit-find-links / --no-emit-find-links Add find-links to generated files (default true) In configuration file, use ``emit_find_links`` option:: [requirements] emit_find_links = False Note: ``uv pip compile`` strips ``--find-links`` entries by default, so this flag is forwarded to both ``pip-tools`` and ``uv`` backends to keep behavior consistent. """ from .base import ClickOption from .forward import ForwardOption class EmitFindLinks(ForwardOption): """Optionally add the find-links entries to the generated files.""" OPTION_NAME = 'emit_find_links' CLICK_OPTION = ClickOption( long_option='--emit-find-links/--no-emit-find-links', is_flag=True, default=True, help_text="Add find-links entries to generated files (default true)", ) enabled_pin_options = ['--emit-find-links'] disabled_pin_options = ['--no-emit-find-links'] ================================================ FILE: pipcompilemulti/features/emit_trusted_host.py ================================================ """ .. _emit_trusted_host: Add trusted host annotation =========================== Control addition of trusted hosts for index URLs to generated files. Trusted hosts can be defined in ``pip.conf`` file, input requirements file, or through an argument to ``pip`` command. Trusted hosts can have invalid HTTPS certificate, or use unencrypted HTTP protocol. By default, trusted hosts are saved in the generated files. Pass ``--no-emit-trusted-host`` to remove it. If no trusted hosts are defined, this flag doesn't have any effect. .. code-block:: text --emit-trusted-host / --no-emit-trusted-host Add trusted host to generated files (default true) In configuration file, use ``emit_trusted_host`` option:: [requirements] emit_trusted_host = False See also: `annotate_index`_ and `extra_index_url`_ options. See pip-tools issue `#382`_ for more details. .. _#382: https://github.com/jazzband/pip-tools/issues/382 """ from .base import ClickOption from .forward import ForwardOption class EmitTrustedHost(ForwardOption): """Optionally add the trusted host to the generated files.""" OPTION_NAME = 'emit_trusted_host' CLICK_OPTION = ClickOption( long_option='--emit-trusted-host/--no-emit-trusted-host', is_flag=True, default=True, help_text="Add trusted host option to generated file" ) enabled_pin_options = ['--emit-trusted-host'] disabled_pin_options = ['--no-emit-trusted-host'] ================================================ FILE: pipcompilemulti/features/extra_index_url.py ================================================ """ .. _extra_index_url: Add additional index URL to search ================================== Pip accepts URLs for additional package indexes through ``--extra-index-url``. The same option can be passed to ``pip-compile-multi`` during compilation. This option is similar to `annotate_index`_. The difference is that the passed URL is not saved in output files, which is helpful if the URL contains private credentials. .. code-block:: text --extra-index-url TEXT Add additional package index URL to search for package versions. Can be supplied multiple times. In configuration file, use ``extra_index_url`` option with comma-separated list of paths:: [requirements] extra_index_url = https://pypi.acme.com/simple .. warning:: Using ``--extra-index-url`` option to search for packages that are not in the main repository (such as private packages) is unsafe, per a security vulnerability called `dependency confusion`_: an attacker can claim the package on the public repository in a way that will ensure it gets chosen over the private package. Use the ``--index-url`` option in pip’s configuration file or command line to specify the feed, overriding the default. Avoid the ``--extra-index-url`` option, which is additive and may lead to having multiple indexes. See `pip documentation`_ for details. .. _pip documentation: https://pip.pypa.io/en/stable/cli/pip_install/#examples .. _dependency confusion: https://azure.microsoft.com/en-us/resources/\ 3-ways-to-mitigate-risk-using-private-package-feeds/ """ from .base import BaseFeature, ClickOption class ExtraIndexUrl(BaseFeature): """Forward extra index URLs to to pip-compile.""" _OPTION = '--extra-index-url' OPTION_NAME = 'extra_index_url' CLICK_OPTION = ClickOption( long_option=_OPTION, multiple=True, help_text=( 'Add additional package index URL to search for package versions. ' 'Can be supplied multiple times.' ) ) def pin_options(self): """Pin command options.""" if self.value: parts = [] for url in self.value: parts.extend([self._OPTION, url]) return parts return [] ================================================ FILE: pipcompilemulti/features/file_extensions.py ================================================ """ Requirements Files Extensions ============================= By default ``pip-compile-multi`` compiles ``*.txt`` from ``*.in`` files. While this is a common naming pattern, each project can use it's own: .. code-block:: text -i, --in-ext TEXT File extension of input files -o, --out-ext TEXT File extension of output files In configuration file, use ``in_ext`` and ``out_ext`` options. For example:: [requirements Hashed] in_ext = txt out_ext = hash """ import os from .base import BaseFeature, ClickOption class InputExtension(BaseFeature): """Override input file extension.""" OPTION_NAME = 'in_ext' CLICK_OPTION = ClickOption( long_option='--in-ext', short_option='-i', default="in", is_flag=False, help_text='File extension of input files.', ) def compose_input_file_name(self, base_name): """Compose file name given environment name. >>> InputExtension().compose_input_file_name('base') 'base.in' """ return '{0}.{1}'.format(base_name, self.value) class OutputExtension(BaseFeature): """Override output file extension.""" OPTION_NAME = 'out_ext' CLICK_OPTION = ClickOption( long_option='--out-ext', short_option='-o', default="txt", is_flag=False, help_text='File extension of output files.', ) def compose_output_file_path(self, in_path): """Compose file name given environment name. >>> OutputExtension().compose_output_file_path('sub/base.in') 'sub/base.txt' """ return '{0}.{1}'.format(os.path.splitext(in_path)[0], self.value) ================================================ FILE: pipcompilemulti/features/forbid_post.py ================================================ """ Forbid .postX release ===================== ``pip-compile-multi`` can remove ``.postX`` part of dependencies versions. .. code-block:: text -p, --forbid-post TEXT Environment name (base, test, etc) that cannot have packages with post-release versions (1.2.3.post777). Can be supplied multiple times. In configuration file, use ``forbid_post`` option with comma-separated list of packages:: [requirements] forbid_post = package1, package2 .. note:: Be careful with this option since different maintainers treat post releases differently. """ from .base import BaseFeature, ClickOption class ForbidPost(BaseFeature): """Truncate postXXX from versions for selected packages.""" OPTION_NAME = 'forbid_post' CLICK_OPTION = ClickOption( long_option='--forbid-post', short_option='-p', multiple=True, help_text="Environment name (base, test, etc) that cannot have " 'packages with post-release versions (1.2.3.post777). ' 'Can be supplied multiple times.' ) @property def enabled_envs(self): """Convert to set.""" return set(self.value or []) @staticmethod def drop_post(version): """Remove .postXXXX postfix from version. >>> ForbidPost.drop_post('1.2.3.post123') '1.2.3' >>> ForbidPost.drop_post('1.2.3') '1.2.3' """ post_index = version.find('.post') if post_index >= 0: return version[:post_index] return version def post_forbidden(self, env_name): """Whether post versions are forbidden for passed environment name.""" return env_name in self.enabled_envs ================================================ FILE: pipcompilemulti/features/forward.py ================================================ """Base feature for forwarding pip-tools options.""" from .base import BaseFeature class ForwardOption(BaseFeature): """Forward command line option to pip-tools.""" #: Pin command options when feature is enabled. enabled_pin_options = [] #: Pin command options when feature is disabled. disabled_pin_options = [] @property def enabled(self): """Is feature enabled.""" return self.value def pin_options(self): """Pin command options.""" if self.enabled: return self.enabled_pin_options return self.disabled_pin_options ================================================ FILE: pipcompilemulti/features/header.py ================================================ """ Custom Header ============= ``pip-compile-multi`` adds a brief header into generated files. Override it with .. code-block:: text -h, --header TEXT File path with custom header text for generated files In configuration file, use ``header_file`` option:: [requirements] header_file = requirements/header.txt """ from .base import BaseFeature, ClickOption DEFAULT_HEADER = """ # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # """.lstrip() class CustomHeader(BaseFeature): """Put custom header at the beginning of locked files.""" OPTION_NAME = 'header_file' CLICK_OPTION = ClickOption( long_option='--header', short_option='-h', default='', help_text='File path with custom header text for generated files.', ) def __init__(self): self._header_text = None @property def text(self): """Text to put in the beginning of each generated file.""" if self._header_text is None: if self.value: self._header_text = self._read_header_text() else: self._header_text = DEFAULT_HEADER return self._header_text def _read_header_text(self): with open(self.value, encoding="utf-8") as fp: return fp.read() ================================================ FILE: pipcompilemulti/features/limit_in_paths.py ================================================ """ .. _limit-in-files: Limit input files ================= By default ``pip-compile-multi`` compiles all ``.in`` files in ``requirements`` directory. To limit compilation to only a subset, use .. code-block:: text -t, --only-path TEXT Compile only for passed input file paths and their references. Can be supplied multiple times. For example, to compile one file under Python2.7 and another under Python3.6, run: .. code-block:: text $ pip-compile-multi -t requirements/deps27.in Locking requirements/deps27.in to requirements/deps27.txt. References: [] $ pip-compile-multi -t requirements/deps36.in Locking requirements/deps36.in to requirements/deps36.txt. References: [] When using ``requirements`` command, use ``include_in_paths`` configuration option with a comma-separated list of paths. For example:: [requirements Python 3.6] include_in_paths = requirements/deps36.in """ from pipcompilemulti.utils import recursive_refs from .base import BaseFeature, ClickOption class LimitInPaths(BaseFeature): """Limit discovered input files to specified subset. >>> from pipcompilemulti.options import OPTIONS >>> feature = LimitInPaths() >>> OPTIONS[feature.OPTION_NAME] = ['test.in'] >>> feature.on_discover([ ... {'in_path': 'base.in', 'refs': []}, ... {'in_path': 'test.in', 'refs': ['base.in']}, ... {'in_path': 'docs.in', 'refs': []}, ... ]) >>> feature.included('base.in') True >>> feature.included('test.in') True >>> feature.included('docs.in') False """ OPTION_NAME = 'include_in_paths' CLICK_OPTION = ClickOption( long_option='--only-path', short_option='-t', multiple=True, help_text='Compile only for passed input paths and their ' 'references. Can be supplied multiple times.', ) def __init__(self): self._all_envs = None @property def direct_envs(self): """Set of environments included by command line options.""" return set(self.value or []) def on_discover(self, env_confs): """Save set of all (recursive) included environments.""" if not self.direct_envs: # No limit means all envs included: self._all_envs = [env['in_path'] for env in env_confs] return transitive_refs = { ref for in_path in self.direct_envs for ref in recursive_refs(env_confs, in_path) } self._all_envs = self.direct_envs | transitive_refs def included(self, in_path): """Whether environment is included directly or by reference.""" return in_path in self._all_envs ================================================ FILE: pipcompilemulti/features/live_output.py ================================================ """ Live output =========== Print debug output from pip-compile live. If the option is disabled (by default) the debug output is printed only in case of failure. .. code-block:: text --live / --no-live Print debug output from pip-compile live. In configuration file, use ``live`` option:: [requirements] live = True """ import subprocess from .base import BaseFeature, ClickOption class LiveOutput(BaseFeature): """Controls whether stdout and stderr should be printed live or at error.""" OPTION_NAME = 'live' CLICK_OPTION = ClickOption( long_option='--live/--no-live', default=False, is_flag=True, help_text='Print debug output from pip-compile live.', ) def pipe_arguments(self): """Values for stdout and stderr arguments to subprocess.Popen.""" if self.value: return {} return { 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE, } ================================================ FILE: pipcompilemulti/features/skip_constraint_comments.py ================================================ """ Skip constraints in comments of output files ============================================ When input files contain constraint references (e.g. '-c constraints.in'), pip-compile adds it to "via" comments. For example:: rsa==3.4.2 # via # -c constraints.txt # google-auth When this option is enabled (default) that snippet will be converted to:: rsa==3.4.2 # via google-auth Saving two lines in .txt file. This feature is especially useful in combination with :ref:`autoresolve`. .. code-block:: text --skip-constraints / --no-skip-constraints Remove constraints from "via" comments. To include constraint comments, set it to ``False`` in a configuration file:: [requirements] skip_constraints = False """ import re from .base import BaseFeature, ClickOption class SkipConstraintComments(BaseFeature): """Remove lines like ``-c file.txt`` from comments in output files.""" OPTION_NAME = 'skip_constraints' CLICK_OPTION = ClickOption( long_option='--skip-constraints/--no-skip-constraints', is_flag=True, default=True, help_text='Remove constraints from "via" comments.', ) _RE_VIA_COMMENT = re.compile( r'^\s*# via$' ) _RE_CONSTRAINT_COMMENT = re.compile( r'^\s*#\s+-c \S+$' ) _RE_PACKAGE_COMMENT = re.compile( r'^\s*#\s+((?:-r )?\S+)$' ) @property def enabled(self): """Whether feature was explicitly enabled or not.""" return self.value def process_dependency_comments(self, comment): """Remove constraint comments if feature is enabled.""" if self.enabled: return self._drop_sink_comment(comment) return comment def _drop_sink_comment(self, comment): r"""Erase sink constraint from comments. >>> feature = SkipConstraintComments() >>> feature._drop_sink_comment("\n# via\n# -c smth\n# pkg\n") '\n# via pkg' >>> feature._drop_sink_comment(" # via pkg") ' # via pkg' """ lines = comment.splitlines() if len(lines) > 2 and self._RE_VIA_COMMENT.match(lines[1]): result = lines[:2] for line in lines[2:]: if self._RE_CONSTRAINT_COMMENT.match(line): continue result.append(line) return "\n".join(self._collapse_single_via(result)) return comment def _collapse_single_via(self, lines): r"""Combine via into a single line when it has only two lines. >>> feature = SkipConstraintComments() >>> feature._collapse_single_via(["", "# via", "# pkg"]) ['', '# via pkg'] >>> feature._collapse_single_via([" # via pkg"]) [' # via pkg'] >>> feature._collapse_single_via(["", "# via", "# -r file"]) ['', '# via -r file'] """ if len(lines) == 3: matchobj = self._RE_PACKAGE_COMMENT.match(lines[2]) if matchobj: package = matchobj.group(1) return [lines[0], lines[1] + ' ' + package] return lines ================================================ FILE: pipcompilemulti/features/strip_extras.py ================================================ """ Strip extras ============ Instructs ``pip-compile`` to attempt to omit extras in transient dependencies, while assuring the constraints compatibility. .. code-block:: text --strip-extras Try avoiding use of extras. In configuration file, use ``strip_extras`` option:: [requirements] strip_extras = True """ from .base import ClickOption from .forward import ForwardOption class StripExtras(ForwardOption): """Attempt to drop extras""" OPTION_NAME = 'strip_extras' CLICK_OPTION = ClickOption( long_option='--strip-extras', is_flag=True, default=False, help_text='Try avoiding use of extras.' ) enabled_pin_options = ['--strip-extras'] ================================================ FILE: pipcompilemulti/features/unsafe.py ================================================ """ Allow Unsafe Packages ===================== If your project depends on packages that include ``setuptools`` or other packages considered "unsafe" by ``pip-tools`` and you still wish to have them included in the resulting requirements files, you can pass this option to do so: .. code-block:: text -s, --allow-unsafe Whether or not to include 'unsafe' packages in generated requirements files. Consult pip-compile --help for more information In configuration file, use ``allow_unsafe`` option:: [requirements] allow_unsafe = True This is commonly used with --generate-hashes to avoid generating requirements files which `cannot be installed`_. .. _cannot be installed: https://github.com/jazzband/pip-tools/issues/806 """ from .base import ClickOption from .forward import ForwardOption class AllowUnsafe(ForwardOption): """Use pip-tools cache, or rebuild from scratch.""" OPTION_NAME = 'allow_unsafe' CLICK_OPTION = ClickOption( long_option='--allow-unsafe', short_option='-s', is_flag=True, default=False, help_text="Whether or not to include 'unsafe' packages " 'in generated requirements files. ' 'Consult pip-compile --help for more information' ) enabled_pin_options = ['--allow-unsafe'] ================================================ FILE: pipcompilemulti/features/upgrade.py ================================================ """ Disable upgrades ================ When new dependencies are added it's tempting to keep everything else the same. To recompile ``.txt`` keeping satisfying version use ``--no-upgrade``: .. code-block:: text --upgrade / --no-upgrade Upgrade package version (default true) The option has no effect if there are no existing ``.txt`` files. When using the ``requirements`` command, disabling upgrades means running ``requirements lock``. Upgrades are enabled when running ``requirements upgrade``. Upgrade only selected packages ============================== To upgrade only one package and keep everything else untouched, use following option: .. code-block:: text -P, --upgrade-package TEXT Only upgrade named package. Can be supplied multiple times. When using the ``requirements`` command, to upgrade only selected packages pass the list of packages to ``requirements upgrade`` command:: requirements upgrade Django cryptography Under the hood it uses `the same option of pip-compile`_ and runs compilation only for files that have one of the passed packages. This option implies ``--no-upgrade`` and takes precedence over ``--upgrade``. Thanks to `Jonathan Rogers `_. .. _`the same option of pip-compile`: \ https://github.com/jazzband/pip-tools#updating-requirements """ import re from .base import BaseFeature, ClickOption from .forward import ForwardOption class UpgradeAll(ForwardOption): """Upgrade all packages in all environments.""" OPTION_NAME = 'upgrade' CLICK_OPTION = ClickOption( long_option='--upgrade/--no-upgrade', default=True, is_flag=True, help_text='Upgrade package version (default true)', ) enabled_pin_options = ['--upgrade'] def __init__(self, controller): self._controller = controller @property def enabled(self): """Whether global upgrade is enabled.""" return self.value and not self._controller.upgrade_selected.active class UpgradeSelected(BaseFeature): """Upgrade only specific packages in all environments.""" OPTION_NAME = 'upgrade_packages' CLICK_OPTION = ClickOption( long_option='--upgrade-package', short_option='-P', multiple=True, help_text='Only upgrade named package. ' 'Can be supplied multiple times.', ) RE_PACKAGE_NAME = re.compile( r'(?iu)(?P[a-z0-9-_.]+)', ) def __init__(self, controller): self._controller = controller self.reset() def reset(self): """Clear cached packages.""" self._env_packages_cache = {} @property def package_specs(self): """List of package specs to upgrade.""" return self.value or [] @property def package_names(self): """List of package names to upgrade.""" def name_from_spec(name): match = self.RE_PACKAGE_NAME.match(name) if match is None: raise ValueError( f"{name!r} does not appear to be a valid package spec", ) return match.group(0) return [name_from_spec(x) for x in self.package_specs] @property def active(self): """Whether selective upgrade is active.""" return bool(self.package_names) def pin_options(self): """Pin command options for upgrading specific packages.""" return [ '--upgrade-package=' + package for package in self.package_specs ] def has_package(self, in_path, package_name): """Whether specified package name is already in the outfile.""" return package_name.lower() in self._get_packages(in_path) def _get_packages(self, in_path): if in_path not in self._env_packages_cache: self._env_packages_cache[in_path] = self._read_packages( self._compose_output_file_path(in_path) ) return self._env_packages_cache[in_path] @staticmethod def _read_packages(outfile): try: with open(outfile, encoding="utf-8") as fp: return set( line.split('==', 1)[0].lower() for line in fp if '==' in line ) except IOError: # Act as if file is empty return set() def _compose_output_file_path(self, in_path): return self._controller.compose_output_file_path(in_path) def affected(self, in_path): """Whether environment was affected by upgraded packages.""" if not self.active: return True return any( self.has_package(in_path, package_name) for package_name in self.package_names ) ================================================ FILE: pipcompilemulti/features/use_cache.py ================================================ """ Use Cache ========= By default ``pip-compile-multi`` executes ``pip-compile`` without ``--rebuild`` flag. ``--rebuild`` flag clears any caches upfront and rebuilds from scratch. Option ``--no-use-cache`` adds ``--rebuild`` flag. .. code-block:: text --use-cache / --no-use-cache Use pip-tools cache to speed up compilation (default true) In configuration file, use ``use_cache`` option:: [requirements] use_cache = False .. note:: But if you run into "magical_" issues, try rerunning compilation with ``--no-use-cache``. .. _magical: https://github.com/jazzband/pip-tools/issues?q=--rebuild """ from .base import ClickOption from .forward import ForwardOption class UseCache(ForwardOption): """Use pip-tools cache, or rebuild from scratch.""" OPTION_NAME = 'use_cache' CLICK_OPTION = ClickOption( long_option='--use-cache/--no-use-cache', is_flag=True, default=True, help_text='Use pip-tools cache to speed up compilation (default true)', ) disabled_pin_options = ['--rebuild'] ================================================ FILE: pipcompilemulti/features/use_uv.py ================================================ """ Enable UV ========= UV is an extremely fast Python package installer and resolver written in Rust. When enabled, pip-compile-multi will use uv's dependency resolver instead of pip-tools. .. code-block:: text --uv / --no-uv Use uv for dependency resolution. In configuration file, use ``uv`` option:: [requirements] uv = True Key differences between uv and pip-tools output: 1. UV is significantly faster at dependency resolution. Particularly noticeable in large projects with complex dependency trees. 2. UV's resolver is more aggressive at finding newer versions. To use UV: - Install ``uv`` (``pip install uv``) - Pass ``--uv`` flag to ``pip-compile-multi`` or add ``uv = True`` when using ``requirements`` command. """ try: import uv # del uv UV_AVAILABLE = True except ImportError: UV_AVAILABLE = False from .base import BaseFeature, ClickOption class UseUV(BaseFeature): """Use uv for dependency resolution. This feature enables using uv's fast Rust-based dependency resolver instead of pip-tools. UV must be installed (pip install uv>=0.1.0) before using this feature. """ OPTION_NAME = 'uv' CLICK_OPTION = ClickOption( long_option='--uv/--no-uv', default=False, is_flag=True, help_text='Use uv for dependency resolution.', ) @staticmethod def is_available(): """Check if uv package is available""" return UV_AVAILABLE ================================================ FILE: pipcompilemulti/options.py ================================================ """Global dictionary holding configuration options.""" OPTIONS = {} ================================================ FILE: pipcompilemulti/utils.py ================================================ """Functional utilities for lists and dicts manipulation.""" import os import logging import itertools logger = logging.getLogger("pip-compile-multi") def extract_env_name(file_path): """Return environment name for given requirements file path. >>> extract_env_name("base.in") 'base' >>> extract_env_name("sub/req.in") 'req' """ return os.path.splitext(os.path.basename(file_path))[0] def fix_reference_path(orig_path, ref_path): """Find actual path to reference using relative path to original file. >>> fix_reference_path("dir/file", "../ref") 'ref' """ return os.path.normpath(os.path.join(os.path.dirname(orig_path), ref_path)) def recursive_refs(envs, in_path): """Return set of recursive refs for given env name.""" refs_by_in_path = { os.path.normpath(env['in_path']): { fix_reference_path(env['in_path'], ref) for ref in env['refs'] } for env in envs } refs = refs_by_in_path[os.path.normpath(in_path)] if refs: indirect_refs = set( subref for ref in refs for subref in recursive_refs(envs, ref) ) else: indirect_refs = set() return set.union(refs, indirect_refs) def merged_packages(env_packages, names): """Return union set of environment packages with given names. >>> sorted(merged_packages( ... { ... 'a': {'x': 1, 'y': 2}, ... 'b': {'y': 2, 'z': 3}, ... 'c': {'z': 3, 'w': 4} ... }, ... ['a', 'b'] ... ).items()) [('x', 1), ('y', 2), ('z', 3)] """ combined_packages = sorted(itertools.chain.from_iterable( env_packages[name].items() for name in names )) result = {} errors = set() for name, version in combined_packages: if name in result: if result[name] != version: errors.add((name, version, result[name])) else: result[name] = version if errors: for error in sorted(errors): logger.error( "Package %s was resolved to different " "versions in different environments: %s and %s", error[0], error[1], error[2], ) raise RuntimeError( "Please add constraints for the package version listed above" ) return result def reference_cluster(envs, in_path): """ Return set of all env in_paths referencing or referenced by given in_path. >>> cluster = sorted(reference_cluster([ ... {'in_path': 'base', 'refs': []}, ... {'in_path': 'test', 'refs': ['base']}, ... {'in_path': 'local', 'refs': ['test']}, ... ], 'test')) >>> cluster == ['base', 'local', 'test'] True """ edges = [ set([env['in_path'], fix_reference_path(env['in_path'], ref)]) for env in envs for ref in env['refs'] ] prev, cluster = set(), set([in_path]) while prev != cluster: # While cluster grows prev = set(cluster) to_visit = [] for edge in edges: if cluster & edge: # Add adjacent nodes: cluster |= edge else: # Leave only edges that are out # of cluster for the next round: to_visit.append(edge) edges = to_visit return cluster ================================================ FILE: pipcompilemulti/verify.py ================================================ """ Check that ``pip-compile-multi`` was run after changes in ``.in`` file ====================================================================== ``pip-compile-multi`` adds a special line (before header) at the beginning of each generated file. This line contains a SHA1 hash of the ``.in`` file's contents. Command .. code-block:: shell $ pip-compile-multi verify OK - requirements/base.txt was generated from requirements/base.in. OK - requirements/test.txt was generated from requirements/test.in. OK - requirements/local.txt was generated from requirements/local.in. OK - requirements/testwin.txt was generated from requirements/testwin.in. Or, if using ``requirements`` command: .. code-block:: shell $ requirements verify OK - requirements/base.txt was generated from requirements/base.in. OK - requirements/test.txt was generated from requirements/test.in. OK - requirements/local.txt was generated from requirements/local.in. OK - requirements/testwin.txt was generated from requirements/testwin.in. OK - requirements/base.hash was generated from requirements/base.txt. OK - requirements/test.hash was generated from requirements/test.txt. OK - requirements/local.hash was generated from requirements/local.txt. OK - requirements/testwin.hash was generated from requirements/testwin.txt. recalculates hashes for ``.in`` files and compares them with the stored values. If verification fails, an error message is logged and exit code 1 is returned: .. code-block:: shell $ pip-compile-multi verify ERROR! requirements/base.txt was not regenerated after changes in requirements/base.in. Expecting: # SHA1:7d82ce5a82b0a6cf91b2c4debe90eb1e5ef37f37 Found: # SHA1:32737333f763ceffd22b7fcb76fbe62a538296fa OK - requirements/test.txt was generated from requirements/test.in. OK - requirements/local.txt was generated from requirements/local.in. OK - requirements/testwin.txt was generated from requirements/testwin.in. In big teams it might be a good idea to have this check in ``tox.ini``: .. code-block:: ini [testenv:verify] skipsdist = true skip_install = true deps = pip-compile-multi commands = pip-compile-multi verify whitelist_externals = pip-compile-multi """ import hashlib import logging from .discover import discover from .environment import Environment from .features import FEATURES logger = logging.getLogger("pip-compile-multi") def verify_environments(): """ For each environment verify hash comments and report failures. If any failure occured, exit with code 1. """ env_confs = discover(FEATURES.compose_input_file_path('*')) success = True for conf in env_confs: env = Environment(in_path=conf['in_path']) current_comment = generate_hash_comment(env.infile) robust_comment = generate_robust_hash_comment(env.infile) existing_comment = parse_hash_comment(env.outfile) if existing_comment in (robust_comment, current_comment): logger.info("OK - %s was generated from %s.", env.outfile, env.infile) else: logger.error("ERROR! %s was not regenerated after changes in %s.", env.outfile, env.infile) logger.error("Expecting: %s", robust_comment.strip()) logger.error("Found: %s", existing_comment.strip()) success = False return success def generate_hash_comment(file_path): """ Read file with given file_path and return string of format # SHA1:da39a3ee5e6b4b0d3255bfef95601890afd80709 which is hex representation of SHA1 file content hash """ with open(file_path, 'rb') as fp: hexdigest = hashlib.sha1(fp.read().strip()).hexdigest() return f"# SHA1:{hexdigest}\n" def generate_robust_hash_comment(file_path): """ Read file with given file_path and return string of format # SHA1:da39a3ee5e6b4b0d3255bfef95601890afd80709 which is hex representation of SHA1 file content hash. File content is pre-processed by stripping comments, whitespace and newlines. """ with open(file_path, 'rt', encoding="utf-8") as fp: essense = ''.join(sorted( line.split('#')[0].strip() for line in fp )) hexdigest = hashlib.sha1(essense.encode("utf-8")).hexdigest() return f"# SHA1:{hexdigest}\n" def parse_hash_comment(file_path): """ Read file with given file_path line by line, return the first line that starts with "# SHA1:", like this: # SHA1:da39a3ee5e6b4b0d3255bfef95601890afd80709 """ with open(file_path, encoding="utf-8") as fp: for line in fp: if line.startswith("# SHA1:"): return line return '' ================================================ FILE: requirements/base.hash ================================================ # SHA1:9afdc18a682b2ce622499600e46778b76e9a9850 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # build==1.5.0 \ --hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \ --hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647 # via # -r requirements/base.txt # pip-tools click==8.3.3 \ --hash=sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2 \ --hash=sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 # via # -r requirements/base.txt # pip-tools packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # -r requirements/base.txt # build # wheel pip==26.1.1 \ --hash=sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb \ --hash=sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78 # via # -r requirements/base.txt # pip-tools pip-tools==7.5.3 \ --hash=sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e \ --hash=sha256:8fa364779ebc010cbfe17cb9de404457ac733e100840423f28f6955de7742d41 # via -r requirements/base.txt pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 # via # -r requirements/base.txt # build # pip-tools setuptools==82.0.1 \ --hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \ --hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb # via # -r requirements/base.txt # pip-tools tomli==2.4.1 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via # -r requirements/base.txt # build # pip-tools toposort==1.10 \ --hash=sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd \ --hash=sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87 # via -r requirements/base.txt wheel==0.47.0 \ --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 # via # -r requirements/base.txt # pip-tools ================================================ FILE: requirements/base.in ================================================ click pip-tools>=6.8.0 toposort ================================================ FILE: requirements/base.txt ================================================ # SHA1:32737333f763ceffd22b7fcb76fbe62a538296fa # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # build==1.5.0 # via pip-tools click==8.3.3 # via # -r requirements/base.in # pip-tools packaging==26.2 # via # build # wheel pip==26.1.1 # via pip-tools pip-tools==7.5.3 # via -r requirements/base.in pyproject-hooks==1.2.0 # via # build # pip-tools setuptools==82.0.1 # via pip-tools tomli==2.4.1 # via # build # pip-tools toposort==1.10 # via -r requirements/base.in wheel==0.47.0 # via pip-tools ================================================ FILE: requirements/local.hash ================================================ # SHA1:08b9c753a9730ea1f06fb8c80c0a3fb45797897c # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # -r test.hash alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via # -r requirements/local.txt # sphinx astroid==4.0.4 \ --hash=sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753 \ --hash=sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0 # via # -r requirements/local.txt # pylint babel==2.18.0 \ --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via # -r requirements/local.txt # sphinx backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via # -r requirements/local.txt # jaraco-context bump2version==1.0.1 \ --hash=sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410 \ --hash=sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6 # via -r requirements/local.txt cachetools==7.1.1 \ --hash=sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d \ --hash=sha256:27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b # via # -r requirements/local.txt # tox certifi==2026.4.22 \ --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 # via # -r requirements/local.txt # requests cffi==2.0.0 \ --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf # via # -r requirements/local.txt # cryptography cfgv==3.5.0 \ --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 # via # -r requirements/local.txt # pre-commit charset-normalizer==3.4.7 \ --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 # via # -r requirements/local.txt # requests collective-checkdocs==0.2 \ --hash=sha256:3a5328257c5224bc72753820c182910d7fb336bc1dba5e09113d48566655e46e # via -r requirements/local.txt colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via # -r requirements/local.txt # tox cryptography==48.0.0 \ --hash=sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13 \ --hash=sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6 \ --hash=sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8 \ --hash=sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25 \ --hash=sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c \ --hash=sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832 \ --hash=sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12 \ --hash=sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c \ --hash=sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7 \ --hash=sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c \ --hash=sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec \ --hash=sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5 \ --hash=sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355 \ --hash=sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c \ --hash=sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741 \ --hash=sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86 \ --hash=sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321 \ --hash=sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a \ --hash=sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7 \ --hash=sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920 \ --hash=sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e \ --hash=sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff \ --hash=sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd \ --hash=sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3 \ --hash=sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f \ --hash=sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602 \ --hash=sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855 \ --hash=sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18 \ --hash=sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a \ --hash=sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336 \ --hash=sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239 \ --hash=sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74 \ --hash=sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a \ --hash=sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c \ --hash=sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4 \ --hash=sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c \ --hash=sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f \ --hash=sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4 \ --hash=sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db \ --hash=sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166 \ --hash=sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5 \ --hash=sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f \ --hash=sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae \ --hash=sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20 \ --hash=sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a \ --hash=sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057 \ --hash=sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb \ --hash=sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c \ --hash=sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b # via # -r requirements/local.txt # secretstorage dill==0.4.1 \ --hash=sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d \ --hash=sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa # via # -r requirements/local.txt # pylint distlib==0.4.0 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d # via # -r requirements/local.txt # virtualenv docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # via # -r requirements/local.txt # collective-checkdocs # readme-renderer # sphinx filelock==3.29.0 \ --hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \ --hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 # via # -r requirements/local.txt # python-discovery # tox # virtualenv flake8==7.3.0 \ --hash=sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e \ --hash=sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872 # via # -r requirements/local.txt # pep8-naming id==1.6.1 \ --hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \ --hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca # via # -r requirements/local.txt # twine identify==2.6.19 \ --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \ --hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842 # via # -r requirements/local.txt # pre-commit idna==3.15 \ --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # -r requirements/local.txt # requests imagesize==2.0.0 \ --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via # -r requirements/local.txt # sphinx importlib-metadata==9.0.0 \ --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via # -r requirements/local.txt # keyring isort==8.0.1 \ --hash=sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d \ --hash=sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75 # via # -r requirements/local.txt # pylint jaraco-classes==3.4.0 \ --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 # via # -r requirements/local.txt # keyring jaraco-context==6.1.2 \ --hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \ --hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3 # via # -r requirements/local.txt # keyring jaraco-functools==4.4.0 \ --hash=sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176 \ --hash=sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb # via # -r requirements/local.txt # keyring jeepney==0.9.0 \ --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ --hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732 # via # -r requirements/local.txt # keyring # secretstorage jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via # -r requirements/local.txt # sphinx keyring==25.7.0 \ --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b # via # -r requirements/local.txt # twine markdown-it-py==4.2.0 \ --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via # -r requirements/local.txt # rich markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via # -r requirements/local.txt # jinja2 mccabe==0.7.0 \ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e # via # -r requirements/local.txt # flake8 # pylint mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via # -r requirements/local.txt # markdown-it-py nh3==0.3.5 \ --hash=sha256:0a09f51806fd51b4fedbf9ea2b61fef388f19aef0d62fe51199d41648be14588 \ --hash=sha256:207c01801d3e9bb8ec08f08689346bdd30ce15b8bf60013a925d08b5388962a4 \ --hash=sha256:23a312224875f72cd16bde417f49071451877e29ef646a60e50fcb69407cc18a \ --hash=sha256:2c069570b06aa848457713ad7af4a9905691291548c4466a9ad78ee95808382b \ --hash=sha256:38748140bf76383ab7ce2dce0ad4cb663855d8fbc9098f7f3483673d09616a17 \ --hash=sha256:387abd011e81959d5a35151a11350a0795c6edeb53ebfa02d2e882dc01299263 \ --hash=sha256:3bb854485c9b33e5bb143ff3e49e577073bc6bc320f0ff8fc316dd89c0d3c101 \ --hash=sha256:45855e14ff056064fec77133bfcf7cd691838168e5e17bbef075394954dc9dc8 \ --hash=sha256:45e6a65dc88a300a2e3502cb9c8e6d1d6b831d6fba7470643333609c6aab1f30 \ --hash=sha256:488928988caad25ba14b1eb5bc74e25e21f3b5e40341d956f3ce4a8bc19460dc \ --hash=sha256:48f45e3e914be93a596431aa143dedf1582557bf41a58153c296048d6e3798c9 \ --hash=sha256:50d401ab2d8e86d59e2126e3ab2a2f45840c405842b626d9a51624b3a33b6878 \ --hash=sha256:52d877980d7ca01dc3baf3936bf844828bc6f332962227a684ed79c18cce14c3 \ --hash=sha256:559e4c73b689e9a7aa97ac9760b1bc488038d7c1a575aa4ab5a0e19ee9630c0f \ --hash=sha256:6ea58cc44d274c643b83547ca9654a0b1a817609b160601356f76a2b744c49ad \ --hash=sha256:72c5bdedec27fa33de6a5326346ea8aa3fe54f6ac294d54c4b204fb66a9f1e79 \ --hash=sha256:84bdeb082544fbcb77a12c034dd77d7da0556fdc0727b787eb6214b958c15e29 \ --hash=sha256:8f85285700a18e9f3fc5bff41fe573fa84f81542ef13b48a89f9fecca0474d3b \ --hash=sha256:acfd354e61accbe4c74f8017c6e397a776916dfe47c48643cf7fd84ade826f93 \ --hash=sha256:c357f1d042c67f135a5e6babb2b0e3b9d9224ff4a3543240f597767b01384ffd \ --hash=sha256:c3aae321f67ae66cff2a627115f106a377d4475d10b0e13d97959a13486b9a88 \ --hash=sha256:c88605d8d468f7fc1b31e06129bc91d6c96f6c621776c9b504a0da9beac9df5f \ --hash=sha256:de8e8621853b6470fe928c684ee0d3f39ea8086cebafe4c416486488dea7b68d \ --hash=sha256:e49c9b564e6bcb03ecd2f057213df9a0de15a95812ac9db9600b590db23d3ae9 \ --hash=sha256:ea232933394d1d58bf7c4bb348dc4660eae6604e1ae81cd2ba6d9ed80d390f3b \ --hash=sha256:eeedc90ed8c42c327e8e10e621ccfa314fc6cce35d5929f4297ff1cdb89667c4 \ --hash=sha256:fe3a787dc76b50de6bee54ef242f26c41dfe47654428e3e94f0fae5bb6dd2cc1 # via # -r requirements/local.txt # readme-renderer nodeenv==1.10.0 \ --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via # -r requirements/local.txt # pre-commit pep8-naming==0.15.1 \ --hash=sha256:eb63925e7fd9e028c7f7ee7b1e413ec03d1ee5de0e627012102ee0222c273c86 \ --hash=sha256:f6f4a499aba2deeda93c1f26ccc02f3da32b035c8b2db9696b730ef2c9639d29 # via -r requirements/local.txt pipdeptree==2.35.2 \ --hash=sha256:5f338ca966f0596c089245324dd6b27031073746d345a6b2b7594450bea82c4a \ --hash=sha256:c8e67055c055cc0966751dc1275c93b5ae05eedee4207cdef543ff4c907061dc # via -r requirements/local.txt pkginfo==1.10.0 \ --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 # via -r requirements/local.txt platformdirs==4.9.6 \ --hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \ --hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917 # via # -r requirements/local.txt # pylint # python-discovery # tox # virtualenv pre-commit==4.6.0 \ --hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \ --hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b # via -r requirements/local.txt pycodestyle==2.14.0 \ --hash=sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783 \ --hash=sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d # via # -r requirements/local.txt # flake8 pycparser==3.0 \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via # -r requirements/local.txt # cffi pydocstyle==6.3.0 \ --hash=sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019 \ --hash=sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1 # via -r requirements/local.txt pyflakes==3.4.0 \ --hash=sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58 \ --hash=sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f # via # -r requirements/local.txt # flake8 pylint==4.0.5 \ --hash=sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2 \ --hash=sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c # via -r requirements/local.txt pyproject-api==1.10.0 \ --hash=sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330 \ --hash=sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09 # via # -r requirements/local.txt # tox python-discovery==1.3.1 \ --hash=sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6 \ --hash=sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c # via # -r requirements/local.txt # tox # virtualenv pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via # -r requirements/local.txt # pre-commit readme-renderer==44.0 \ --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 # via # -r requirements/local.txt # twine requests==2.34.0 \ --hash=sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a \ --hash=sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60 # via # -r requirements/local.txt # requests-toolbelt # sphinx # twine requests-toolbelt==1.0.0 \ --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 # via # -r requirements/local.txt # twine rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via # -r requirements/local.txt # twine rich==15.0.0 \ --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # -r requirements/local.txt # twine secretstorage==3.5.0 \ --hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \ --hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be # via # -r requirements/local.txt # keyring snowballstemmer==3.0.1 \ --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 # via # -r requirements/local.txt # pydocstyle # sphinx sphinx==8.1.3 \ --hash=sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2 \ --hash=sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927 # via -r requirements/local.txt sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 # via # -r requirements/local.txt # sphinx sphinxcontrib-devhelp==2.0.0 \ --hash=sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad \ --hash=sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 # via # -r requirements/local.txt # sphinx sphinxcontrib-htmlhelp==2.1.0 \ --hash=sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8 \ --hash=sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9 # via # -r requirements/local.txt # sphinx sphinxcontrib-jsmath==1.0.1 \ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 # via # -r requirements/local.txt # sphinx sphinxcontrib-qthelp==2.0.0 \ --hash=sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab \ --hash=sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb # via # -r requirements/local.txt # sphinx sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via # -r requirements/local.txt # sphinx tomli-w==1.2.0 \ --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 # via # -r requirements/local.txt # tox tomlkit==0.15.0 \ --hash=sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738 \ --hash=sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3 # via # -r requirements/local.txt # pylint tox==4.54.0 \ --hash=sha256:21e36fd8256590379620848d0b03b52f4d541b65b749de1a17c3e616978dad58 \ --hash=sha256:a2d7c1177242ae9c3d9e404039e9f945ce16a3e5dfc66972c643e27d7e764f4b # via -r requirements/local.txt twine==6.2.0 \ --hash=sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8 \ --hash=sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf # via -r requirements/local.txt urllib3==2.7.0 \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # -r requirements/local.txt # id # requests # twine virtualenv==21.3.2 \ --hash=sha256:3ecda97894a6fc1c53106356f488690e5c86278c1f693f3fc0805ac85a513686 \ --hash=sha256:c58ea748fa50bb2a4367da5ba3d30b02458ed40b4ea888faad94021f3309f764 # via # -r requirements/local.txt # pre-commit # tox zipp==3.23.1 \ --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 # via # -r requirements/local.txt # importlib-metadata ================================================ FILE: requirements/local.in ================================================ -r test.in tox twine wheel bump2version flake8 collective.checkdocs pygments pre-commit pipdeptree pylint pep8-naming pycodestyle pydocstyle pylint sphinx virtualenv pkginfo<1.11 ================================================ FILE: requirements/local.txt ================================================ # SHA1:e617f45ac71d5408e21d0e8432038f3deb4c1e2d # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # -r test.txt alabaster==1.0.0 # via sphinx astroid==4.0.4 # via pylint babel==2.18.0 # via sphinx backports-tarfile==1.2.0 # via jaraco-context bump2version==1.0.1 # via -r requirements/local.in cachetools==7.1.1 # via tox certifi==2026.4.22 # via requests cffi==2.0.0 # via cryptography cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests collective-checkdocs==0.2 # via -r requirements/local.in colorama==0.4.6 # via tox cryptography==48.0.0 # via secretstorage dill==0.4.1 # via pylint distlib==0.4.0 # via virtualenv docutils==0.21.2 # via # collective-checkdocs # readme-renderer # sphinx filelock==3.29.0 # via # python-discovery # tox # virtualenv flake8==7.3.0 # via # -r requirements/local.in # pep8-naming id==1.6.1 # via twine identify==2.6.19 # via pre-commit idna==3.15 # via requests imagesize==2.0.0 # via sphinx importlib-metadata==9.0.0 # via keyring isort==8.0.1 # via pylint jaraco-classes==3.4.0 # via keyring jaraco-context==6.1.2 # via keyring jaraco-functools==4.4.0 # via keyring jeepney==0.9.0 # via # keyring # secretstorage jinja2==3.1.6 # via sphinx keyring==25.7.0 # via twine markdown-it-py==4.2.0 # via rich markupsafe==3.0.3 # via jinja2 mccabe==0.7.0 # via # flake8 # pylint mdurl==0.1.2 # via markdown-it-py nh3==0.3.5 # via readme-renderer nodeenv==1.10.0 # via pre-commit pep8-naming==0.15.1 # via -r requirements/local.in pipdeptree==2.35.2 # via -r requirements/local.in pkginfo==1.10.0 # via -r requirements/local.in platformdirs==4.9.6 # via # pylint # python-discovery # tox # virtualenv pre-commit==4.6.0 # via -r requirements/local.in pycodestyle==2.14.0 # via # -r requirements/local.in # flake8 pycparser==3.0 # via cffi pydocstyle==6.3.0 # via -r requirements/local.in pyflakes==3.4.0 # via flake8 pylint==4.0.5 # via -r requirements/local.in pyproject-api==1.10.0 # via tox python-discovery==1.3.1 # via # tox # virtualenv pyyaml==6.0.3 # via pre-commit readme-renderer==44.0 # via twine requests==2.34.0 # via # requests-toolbelt # sphinx # twine requests-toolbelt==1.0.0 # via twine rfc3986==2.0.0 # via twine rich==15.0.0 # via twine secretstorage==3.5.0 # via keyring snowballstemmer==3.0.1 # via # pydocstyle # sphinx sphinx==8.1.3 # via -r requirements/local.in sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 # via sphinx sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx tomli-w==1.2.0 # via tox tomlkit==0.15.0 # via pylint tox==4.54.0 # via -r requirements/local.in twine==6.2.0 # via -r requirements/local.in urllib3==2.7.0 # via # id # requests # twine virtualenv==21.3.2 # via # -r requirements/local.in # pre-commit # tox zipp==3.23.1 # via importlib-metadata ================================================ FILE: requirements/test.hash ================================================ # SHA1:1ecb652424b040b181164313dd881f4f3ee1477f # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # -r base.hash coverage==7.14.0 \ --hash=sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74 \ --hash=sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e \ --hash=sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a \ --hash=sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c \ --hash=sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a \ --hash=sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe \ --hash=sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1 \ --hash=sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db \ --hash=sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9 \ --hash=sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e \ --hash=sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b \ --hash=sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66 \ --hash=sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644 \ --hash=sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490 \ --hash=sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5 \ --hash=sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8 \ --hash=sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f \ --hash=sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212 \ --hash=sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb \ --hash=sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917 \ --hash=sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893 \ --hash=sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c \ --hash=sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90 \ --hash=sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d \ --hash=sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef \ --hash=sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9 \ --hash=sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087 \ --hash=sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5 \ --hash=sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27 \ --hash=sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020 \ --hash=sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3 \ --hash=sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47 \ --hash=sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca \ --hash=sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323 \ --hash=sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d \ --hash=sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd \ --hash=sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426 \ --hash=sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828 \ --hash=sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480 \ --hash=sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97 \ --hash=sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8 \ --hash=sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899 \ --hash=sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2 \ --hash=sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5 \ --hash=sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3 \ --hash=sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20 \ --hash=sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436 \ --hash=sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327 \ --hash=sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b \ --hash=sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb \ --hash=sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe \ --hash=sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab \ --hash=sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82 \ --hash=sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627 \ --hash=sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5 \ --hash=sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3 \ --hash=sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477 \ --hash=sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662 \ --hash=sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075 \ --hash=sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f \ --hash=sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1 \ --hash=sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7 \ --hash=sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52 \ --hash=sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d \ --hash=sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9 \ --hash=sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed \ --hash=sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90 \ --hash=sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1 \ --hash=sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d \ --hash=sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980 \ --hash=sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca \ --hash=sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa \ --hash=sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6 \ --hash=sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc \ --hash=sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4 \ --hash=sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c \ --hash=sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d \ --hash=sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84 \ --hash=sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2 \ --hash=sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742 \ --hash=sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2 \ --hash=sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb \ --hash=sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a \ --hash=sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9 \ --hash=sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96 \ --hash=sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f \ --hash=sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb \ --hash=sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63 \ --hash=sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c \ --hash=sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1 \ --hash=sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec \ --hash=sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367 \ --hash=sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20 \ --hash=sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67 \ --hash=sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b \ --hash=sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85 \ --hash=sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0 \ --hash=sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4 \ --hash=sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7 \ --hash=sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218 \ --hash=sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757 \ --hash=sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef \ --hash=sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1 \ --hash=sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea \ --hash=sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae \ --hash=sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595 # via # -r requirements/test.txt # pytest-cov exceptiongroup==1.3.1 \ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via # -r requirements/test.txt # pytest iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via # -r requirements/test.txt # pytest mock==5.2.0 \ --hash=sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0 \ --hash=sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f # via -r requirements/test.txt more-itertools==11.0.2 \ --hash=sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804 \ --hash=sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4 # via -r requirements/test.txt pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via # -r requirements/test.txt # pytest # pytest-cov pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via # -r requirements/test.txt # pytest pytest==9.0.3 \ --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c # via # -r requirements/test.txt # pytest-cov pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 # via -r requirements/test.txt typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # -r requirements/test.txt # exceptiongroup uv==0.11.14 \ --hash=sha256:078f2e63da89c8fcf6d578f02156045c5990c57d76464aab3f3f798d3fff95cd \ --hash=sha256:0ea006a117b586b2681b6dfd9703a540d2ad2a136ec0f48d272767e599cc3dfb \ --hash=sha256:29c12a562441fc2d604e6920c558cacce74a55f889468708683a79b35a6e18a1 \ --hash=sha256:379e64b236cf55f762a8308d7efe4365d5296ba29f3a4868761bc45b4e915a71 \ --hash=sha256:3b0759ca504e48dcd4fafb1a61ef69aeb24c5a60fbf5f504a7873c8db1b24718 \ --hash=sha256:6a13e7e064563050c6606b3fd77091d427cdbdc5938b6f134baf8d8ec79bfdb7 \ --hash=sha256:78411a883f230a710af19f2ac6e6f0ba8eae90f0e5af4605f923fd367539fff4 \ --hash=sha256:78b51b117549ee4db7197ea5ece0848cecd443e464fb9dff9f254cdc1e4ed96f \ --hash=sha256:9923da7c63d70de9fe71829503d7e7ebfd6304e804d7232aad5f716e190db25b \ --hash=sha256:a1ddbe8a2ab160affc179e9c3a40913b23a08cdf55254e1f3829cc22a51a0d8d \ --hash=sha256:b15bf7c146e38d7c938d3a207115d5fdd8ef764fe1f866c225b1bed27e88da1e \ --hash=sha256:b384d873d0d18552c7524226125efd3965d921b7134c2f476c333771beb733e1 \ --hash=sha256:d5c8f9ea36274ef2f9d24f0522085e280844172e901d9213f66a21b212266706 \ --hash=sha256:dcdad43d52c130e3159e84ab1844e04d819d2c4a2495a687d27f80d560a3650e \ --hash=sha256:ddda5c5e41097814adac535c74851bae55e8097b9afc79aeae7fcffd8d86c06d \ --hash=sha256:e54326703f1eca83a6fd73275e0f398b16b7d3f81531bf58899c2869bc403f6c \ --hash=sha256:e84069681c0334e07cbc7f114eb09d7fe1335e1db0297a66dbca80a1b393fe6d \ --hash=sha256:f0a8b58b38e984241bca5d7a5a47bf9ffe1ca2ab392a640887db8a04c4a9ec95 \ --hash=sha256:f3005a2db1e8d72e125630d4f22ac4ceddb2c033e1f9b94b7f3ea38ebac46dd6 # via -r requirements/test.txt ================================================ FILE: requirements/test.in ================================================ -r base.in pytest pytest-cov mock more-itertools uv>=0.1.0 # Astral's faster dependency resolver ================================================ FILE: requirements/test.txt ================================================ # SHA1:93b27adc99cd695162fe4433a52e3d0639d8b812 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # -r base.txt coverage==7.14.0 # via pytest-cov exceptiongroup==1.3.1 # via pytest iniconfig==2.3.0 # via pytest mock==5.2.0 # via -r requirements/test.in more-itertools==11.0.2 # via -r requirements/test.in pluggy==1.6.0 # via # pytest # pytest-cov pygments==2.20.0 # via pytest pytest==9.0.3 # via # -r requirements/test.in # pytest-cov pytest-cov==7.1.0 # via -r requirements/test.in typing-extensions==4.15.0 # via exceptiongroup uv==0.11.14 # via -r requirements/test.in ================================================ FILE: requirements/testwin.hash ================================================ # SHA1:a053c9dcc1ff686538ecf3dda5723ad59a09a7cd # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r test.hash atomicwrites==1.4.0 \ --hash=sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197 \ --hash=sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a # via -r requirements/testwin.txt colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via -r requirements/testwin.txt # The following packages are considered to be unsafe in a requirements file: ================================================ FILE: requirements/testwin.in ================================================ -r test.in colorama atomicwrites ================================================ FILE: requirements/testwin.txt ================================================ # SHA1:ab0b9abf8863d5dced78a6f5664a664d8d2488cd # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r test.txt atomicwrites==1.4.0 # via -r requirements/testwin.in colorama==0.4.4 # via -r requirements/testwin.in # The following packages are considered to be unsafe in a requirements file: # pip # setuptools ================================================ FILE: setup.cfg ================================================ [bumpversion] current_version = 3.3.1 commit = True tag = True [wheel] universal = 1 [bdist_wheel] universal = 1 [bumpversion:file:setup.py] search = VERSION = "{current_version}" replace = VERSION = "{new_version}" [bumpversion:file:pipcompilemulti/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' [requirements:Python 3] python = 3.10 autoresolve = True include_in_paths = requirements/local.in skip_constraints = True allow_unsafe = True use_cache = True uv = True [requirements:Python 3 hash] python = 3.10 include_in_paths = requirements/local.txt generate_hashes = local in_ext = txt out_ext = hash allow_unsafe = True use_cache = True uv = True [pycodestyle] max-line-length = 120 [flake8] max-line-length = 120 ================================================ FILE: setup.py ================================================ """Package configuration""" import os from setuptools import setup, find_packages VERSION = "3.3.1" README = """ pip-compile-multi ================= Compile multiple requirements files to lock dependency versions. Install ------- .. code-block:: shell pip install pip_compile_multi Run ---- .. code-block:: shell pip-compile-multi Links ----- * Documentation: https://pip-compile-multi.readthedocs.io/en/latest/ * Releases: https://pypi.python.org/pypi/pip-compile-multi * Code: https://github.com/peterdemin/pip-compile-multi * Issue tracker: https://github.com/peterdemin/pip-compile-multi/issues """ with open('HISTORY.rst', encoding='utf-8') as fp: HISTORY = fp.read().replace('.. :changelog:', '') with open(os.path.join('requirements', 'base.in'), encoding='utf-8') as fp: REQUIREMENTS = list(fp) CONSOLE_SCRIPTS = [ 'pip-compile-multi = pipcompilemulti.cli_v1:cli', 'requirements = pipcompilemulti.cli_v2:cli', ] setup( name='pip_compile_multi', version=VERSION, description="Compile multiple requirements files " "to lock dependency versions", long_description=README + '\n\n' + HISTORY, author='Peter Demin', author_email='peterdemin@gmail.com', url='https://github.com/peterdemin/pip-compile-multi', include_package_data=True, packages=find_packages(exclude=['tests']), install_requires=REQUIREMENTS, python_requires='~=3.10', license="MIT", zip_safe=False, keywords='pip-compile-multi', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Natural Language :: English', 'Environment :: Console', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Utilities', ], entry_points={ 'console_scripts': CONSOLE_SCRIPTS, }, setup_requires=['setuptools', 'wheel'], ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/configs/pyproject.toml ================================================ [tool.requirements.one] uv=true [tool.requirements.two] generate_hashes="file.txt" ================================================ FILE: tests/configs/requirements.ini ================================================ [requirements] allow_unsafe = True use_cache = True uv = True ================================================ FILE: tests/configs/setup.cfg ================================================ [requirements:Python 3] autoresolve = True ================================================ FILE: tests/conflicting-in-merge/base1.in ================================================ pytz<2018 ================================================ FILE: tests/conflicting-in-merge/base1.txt ================================================ # SHA1:07973b0a74462beb75c2bb65cae3db0b53237103 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # pytz==2017.3 # via -r tests/conflicting-in-merge/base1.in ================================================ FILE: tests/conflicting-in-merge/base2.in ================================================ pytz ================================================ FILE: tests/conflicting-in-merge/base2.txt ================================================ # SHA1:fe99a8ac13be13203ced26880be5ac493ae359ab # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # pytz==2021.3 # via -r tests/conflicting-in-merge/base2.in ================================================ FILE: tests/conflicting-in-merge/together.in ================================================ -r base1.in -r base2.in ================================================ FILE: tests/conflicting-in-merge/together.txt ================================================ # SHA1:08d9c33c7a4a6c4972d515c176318068baf08856 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r base1.txt -r base2.txt ================================================ FILE: tests/conflicting-in-ref/base1.in ================================================ Django==5.2.12 ================================================ FILE: tests/conflicting-in-ref/base1.txt ================================================ # SHA1:b6c95e2580609775eb2b7302ad50c369810daef6 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # asgiref==3.4.1 # via django django==3.2.11 # via -r tests/conflicting-in-ref/base1.in pytz==2021.3 # via django sqlparse==0.4.2 # via django typing-extensions==4.0.1 # via asgiref ================================================ FILE: tests/conflicting-in-ref/base2.in ================================================ -r base1.in opal==0.21.0 ================================================ FILE: tests/conflicting-in-ref/base2.txt ================================================ amqp==5.0.9 # via kombu billiard==3.6.4.0 # via celery cached-property==1.5.2 # via kombu celery==5.0.2 # via # django-celery-results # opal certifi==2021.10.8 # via requests chardet==3.0.4 # via requests click==8.0.3 # via # celery # click-didyoumean # click-repl click-didyoumean==0.3.0 # via celery click-repl==0.2.0 # via celery django==2.2.16 # via # -r tests/conflicting-in-ref/base1.in # django-appconf # django-reversion # djangorestframework # opal django-appconf==1.0.5 # via django-compressor django-celery-results==2.0.0 # via opal django-compressor==2.4 # via opal django-reversion==3.0.8 # via opal djangorestframework==3.12.2 # via opal ffs==0.0.8.2 # via opal idna==2.10 # via requests importlib-metadata==4.8.3 # via # click # kombu jinja2==2.10.1 # via opal kombu==5.1.0 # via celery markupsafe==1.1.1 # via # jinja2 # opal opal==0.21.0 # via -r tests/conflicting-in-ref/base2.in prompt-toolkit==3.0.24 # via click-repl python-dateutil==2.8.1 # via opal pytz==2021.3 # via # celery # django rcssmin==1.0.6 # via django-compressor requests==2.25.0 # via opal rjsmin==1.1.0 # via django-compressor six==1.15.0 # via # click-repl # django-compressor # ffs # opal # python-dateutil sqlparse==0.4.2 # via django typing-extensions==4.0.1 # via importlib-metadata urllib3==1.26.8 # via requests vine==5.0.0 # via # amqp # celery # kombu wcwidth==0.2.5 # via prompt-toolkit zipp==3.6.0 # via importlib-metadata ================================================ FILE: tests/conftest.py ================================================ """Pytest configuration.""" import shutil import pathlib import os.path import tempfile import contextlib import pytest from click.testing import CliRunner from pipcompilemulti.options import OPTIONS HERE = os.path.dirname(os.path.abspath(__file__)) @pytest.fixture() def runner(): """Fixture for invoking CLI commands.""" return CliRunner() @pytest.fixture(autouse=True) def wipe_options(): """Reset global OPTIONS dictionary before every test.""" OPTIONS.clear() @pytest.fixture() def test_data_tmpdir(): """Copy the requested test data to a temporary directory.""" with contextlib.ExitStack() as stack: def copy(test_data_name): source = os.path.join(HERE, test_data_name) tmp_dir = stack.enter_context(tempfile.TemporaryDirectory()) os.rmdir(tmp_dir) shutil.copytree(source, tmp_dir) return pathlib.Path(tmp_dir) yield copy ================================================ FILE: tests/sys_platform/base.in ================================================ torch==1.12.0; sys_platform=='darwin' and python_version > '3.6' # Mac OS torch==1.11.0; sys_platform=='linux' # Linux pip-compile-multi @ https://github.com/peterdemin/pip-compile-multi/archive/refs/tags/v2.4.5.tar.gz ; sys_platform=='darwin' https://github.com/peterdemin/pip-compile-multi/archive/refs/tags/v2.4.5.tar.gz ; sys_platform=='darwin' ================================================ FILE: tests/sys_platform/base.txt ================================================ # SHA1:854aa2388eff8ba43b94aee23c77726c4fa6a05d # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # build==0.8.0 \ --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \ --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0 # via pip-tools click==8.1.3 \ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 # via # pip-compile-multi # pip-tools packaging==21.3 \ --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 # via build pep517==0.12.0 \ --hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \ --hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161 # via build pip-compile-multi @ https://github.com/peterdemin/pip-compile-multi/archive/refs/tags/v2.4.5.tar.gz ; sys_platform == "darwin" --hash=sha256:d5cefaa1033e53ef39a59b765cd6065aee0af9e03298d74c5c9bb3d8087b9b3f # via -r tests/sys_platform/base.in pip-tools==6.8.0 \ --hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \ --hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8 # via pip-compile-multi pyparsing==3.0.9 \ --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc # via packaging tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via # build # pep517 toposort==1.7 \ --hash=sha256:8ed8e109e96ae30bf66da2d2155e4eb9989d9c5c743c837e37d9774a4eddd804 \ --hash=sha256:ddc2182c42912a440511bd7ff5d3e6a1cabc3accbc674a3258c8c41cbfbb2125 # via pip-compile-multi torch==1.12.0 ; sys_platform == "darwin" and python_version > "3.6" \ --hash=sha256:0399746f83b4541bcb5b219a18dbe8cade760aba1c660d2748a38c6dc338ebc7 \ --hash=sha256:0986685f2ec8b7c4d3593e8cfe96be85d462943f1a8f54112fc48d4d9fbbe903 \ --hash=sha256:13c7cca6b2ea3704d775444f02af53c5f072d145247e17b8cd7813ac57869f03 \ --hash=sha256:201abf43a99bb4980cc827dd4b38ac28f35e4dddac7832718be3d5479cafd2c1 \ --hash=sha256:2143d5fe192fd908b70b494349de5b1ac02854a8a902bd5f47d13d85b410e430 \ --hash=sha256:2568f011dddeb5990d8698cc375d237f14568ffa8489854e3b94113b4b6b7c8b \ --hash=sha256:3322d33a06e440d715bb214334bd41314c94632d9a2f07d22006bf21da3a2be4 \ --hash=sha256:349ea3ba0c0e789e0507876c023181f13b35307aebc2e771efd0e045b8e03e84 \ --hash=sha256:44a3804e9bb189574f5d02ccc2dc6e32e26a81b3e095463b7067b786048c6072 \ --hash=sha256:5ed69d5af232c5c3287d44cef998880dadcc9721cd020e9ae02f42e56b79c2e4 \ --hash=sha256:60d06ee2abfa85f10582d205404d52889d69bcbb71f7e211cfc37e3957ac19ca \ --hash=sha256:63341f96840a223f277e498d2737b39da30d9f57c7a1ef88857b920096317739 \ --hash=sha256:72207b8733523388c49d43ffcc4416d1d8cd64c40f7826332e714605ace9b1d2 \ --hash=sha256:7ddb167827170c4e3ff6a27157414a00b9fef93dea175da04caf92a0619b7aee \ --hash=sha256:844f1db41173b53fe40c44b3e04fcca23a6ce00ac328b7099f2800e611766845 \ --hash=sha256:a1325c9c28823af497cbf443369bddac9ac59f67f1e600f8ab9b754958e55b76 \ --hash=sha256:abbdc5483359b9495dc76e3bd7911ccd2ddc57706c117f8316832e31590af871 \ --hash=sha256:c0313438bc36448ffd209f5fb4e5f325b3af158cdf61c8829b8ddaf128c57816 \ --hash=sha256:e3e8348edca3e3cee5a67a2b452b85c57712efe1cc3ffdb87c128b3dde54534e \ --hash=sha256:fb47291596677570246d723ee6abbcbac07eeba89d8f83de31e3954f21f44879 # via -r tests/sys_platform/base.in typing-extensions==4.3.0 \ --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 # via torch wheel==0.37.1 \ --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \ --hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4 # via pip-tools # WARNING: The following packages were not pinned, but pip requires them to be # pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # pip # setuptools ================================================ FILE: tests/sys_platform/test.in ================================================ -r base.in torchmetrics # Metrics comment ================================================ FILE: tests/sys_platform/test.txt ================================================ # SHA1:97dcc9f96f311da386d4460771c4ecb4a47dfe6d # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r base.txt numpy==1.23.1 \ --hash=sha256:1408c3527a74a0209c781ac82bde2182b0f0bf54dea6e6a363fe0cc4488a7ce7 \ --hash=sha256:173f28921b15d341afadf6c3898a34f20a0569e4ad5435297ba262ee8941e77b \ --hash=sha256:1865fdf51446839ca3fffaab172461f2b781163f6f395f1aed256b1ddc253622 \ --hash=sha256:3119daed207e9410eaf57dcf9591fdc68045f60483d94956bee0bfdcba790953 \ --hash=sha256:35590b9c33c0f1c9732b3231bb6a72d1e4f77872390c47d50a615686ae7ed3fd \ --hash=sha256:37e5ebebb0eb54c5b4a9b04e6f3018e16b8ef257d26c8945925ba8105008e645 \ --hash=sha256:37ece2bd095e9781a7156852e43d18044fd0d742934833335599c583618181b9 \ --hash=sha256:3ab67966c8d45d55a2bdf40701536af6443763907086c0a6d1232688e27e5447 \ --hash=sha256:47f10ab202fe4d8495ff484b5561c65dd59177949ca07975663f4494f7269e3e \ --hash=sha256:55df0f7483b822855af67e38fb3a526e787adf189383b4934305565d71c4b148 \ --hash=sha256:5d732d17b8a9061540a10fda5bfeabca5785700ab5469a5e9b93aca5e2d3a5fb \ --hash=sha256:68b69f52e6545af010b76516f5daaef6173e73353e3295c5cb9f96c35d755641 \ --hash=sha256:7e8229f3687cdadba2c4faef39204feb51ef7c1a9b669247d49a24f3e2e1617c \ --hash=sha256:8002574a6b46ac3b5739a003b5233376aeac5163e5dcd43dd7ad062f3e186129 \ --hash=sha256:876f60de09734fbcb4e27a97c9a286b51284df1326b1ac5f1bf0ad3678236b22 \ --hash=sha256:9ce242162015b7e88092dccd0e854548c0926b75c7924a3495e02c6067aba1f5 \ --hash=sha256:a35c4e64dfca659fe4d0f1421fc0f05b8ed1ca8c46fb73d9e5a7f175f85696bb \ --hash=sha256:aeba539285dcf0a1ba755945865ec61240ede5432df41d6e29fab305f4384db2 \ --hash=sha256:b15c3f1ed08df4980e02cc79ee058b788a3d0bef2fb3c9ca90bb8cbd5b8a3a04 \ --hash=sha256:c2f91f88230042a130ceb1b496932aa717dcbd665350beb821534c5c7e15881c \ --hash=sha256:d748ef349bfef2e1194b59da37ed5a29c19ea8d7e6342019921ba2ba4fd8b624 \ --hash=sha256:e0d7447679ae9a7124385ccf0ea990bb85bb869cef217e2ea6c844b6a6855073 # via torchmetrics torchmetrics==0.9.2 \ --hash=sha256:8178c9242e243318093d9b7237738a504535193d2006da6e58b0ed4003e318d2 \ --hash=sha256:ced006295c95c4555df0b8dea92960c00e3303de0da878fcf27e394df4757827 # via -r tests/sys_platform/test.in # WARNING: The following packages were not pinned, but pip requires them to be # pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # pip # setuptools ================================================ FILE: tests/test_add_hashes.py ================================================ # pylint: disable=too-few-public-methods,missing-module-docstring # pylint: disable=missing-function-docstring,missing-class-docstring import unittest from pipcompilemulti.options import OPTIONS from pipcompilemulti.features.add_hashes import AddHashes class AddHashesTestCase(unittest.TestCase): def setUp(self): self._add_hashes = AddHashes(FakeController()) OPTIONS[self._add_hashes.OPTION_NAME] = ['test'] self._add_hashes.on_discover([ {'in_path': 'base', 'refs': []}, {'in_path': 'test', 'refs': ['base']}, {'in_path': 'docs', 'refs': []}, ]) def test_pin_options(self): assert self._add_hashes.pin_options('base') == ['--generate-hashes'] assert not self._add_hashes.pin_options('docs') class FakeController(): def compose_input_file_path(self, name): return name ================================================ FILE: tests/test_cli_v1.py ================================================ """End to end tests for CLI v1""" from pathlib import Path import pytest from click.testing import CliRunner from pipcompilemulti import environment, verify from pipcompilemulti.cli_v1 import cli from .utils import temp_dir HERE = Path(__file__).parent @pytest.fixture(autouse=True) def requirements_dir(): """Create temporary requirements directory for test time.""" with temp_dir(): yield @pytest.mark.parametrize('command', ['--no-upgrade', '--upgrade', '--upgrade-package=pip-tools']) def test_v1_command_exits_with_zero(command, monkeypatch): """Run pip-compile-multi on self. pip-compile-multi --only-path local.txt --generate-hashes local \ --in-ext txt --out-ext hash --use-cache \ --autoresolve """ # Mock version check to avoid conflicts original_fix_pin = environment.Environment.fix_pin def mock_fix_pin(self, section): if 'coverage[toml]' in section or 'more-itertools' in section: return section return original_fix_pin(self, section) monkeypatch.setattr('pipcompilemulti.environment.Environment.fix_pin', mock_fix_pin) runner = CliRunner() requirements = Path('requirements') common_parameters = ['--autoresolve', '--use-cache'] parameters = common_parameters + [ command, '--only-path', str(requirements / 'local.in'), ] result = runner.invoke(cli, parameters, catch_exceptions=False) assert result.exit_code == 0 parameters = common_parameters + [ '--no-upgrade', '--generate-hashes', 'local', '--in-ext', 'txt', '--out-ext', 'hash', '--only-path', str(requirements / 'local.txt'), ] result = runner.invoke(cli, parameters, catch_exceptions=False) assert result.exit_code == 0 def test_v1_verify_exits_with_zero(monkeypatch): """Run pip-compile-multi on self""" # Mock discover to return empty list monkeypatch.setattr(verify, 'discover', lambda _: []) runner = CliRunner() result = runner.invoke(cli, ['verify']) assert result.exit_code == 0 def _load_tree(root, replace_name=None): if not replace_name: replace_name = str(root).replace('\\', '/') return { x.relative_to(root).name: x.read_text().replace(replace_name, 'ROOT').replace('\\', '/') for x in root.glob('**/*.txt') } @pytest.mark.parametrize('name, args', [ ('upgrade', ['-P', 'markupsafe']), ('upgrade-with-range', ['-P', 'markupsafe<2.1.2']), ('upgrade-autoresolve-with-range', ['--autoresolve', '-P', 'markupsafe<2.1.2']), ]) def test_package_upgrade(test_data_tmpdir, name, args): """Run pip-compile-multi with various upgrade arguments""" working_root = test_data_tmpdir(name) runner = CliRunner() result = runner.invoke( cli, [*args, '--directory', str(working_root)], catch_exceptions=False, ) assert result.exit_code == 0 expected = _load_tree( HERE / f'{name}-expected', # Cope with posix style reference data on Windows replace_name=f'tests/{name}-expected', ) actual = _load_tree(working_root) assert actual == expected ================================================ FILE: tests/test_cli_v2.py ================================================ """End to end tests for CLI v2""" from click.testing import CliRunner import pytest from pipcompilemulti.cli_v2 import cli from .utils import temp_dir @pytest.fixture(autouse=True) def requirements_dir(): """Create temporary requirements directory for test time.""" with temp_dir(): yield @pytest.mark.parametrize('command', ['lock', 'upgrade', 'verify']) def test_command_exits_with_zero(command): """Run requirements command on self""" runner = CliRunner() result = runner.invoke(cli, [command]) assert result.exit_code == 0 ================================================ FILE: tests/test_config.py ================================================ """Tests for config loading for CLI v2""" import os import pathlib import shutil import tempfile from typing import Iterator, List import pytest from pipcompilemulti.config import read_config TESTDATA = pathlib.Path(__file__).parent / 'configs' ASSETS = { path.name: path.read_text(encoding='utf-8') for path in TESTDATA.glob('*.*') if not path.name.startswith('.') } def _write_asset(name: str) -> None: pathlib.Path(name).write_text(ASSETS[name], encoding='utf-8') @pytest.fixture(autouse=True) def in_temp_dir() -> Iterator[str]: """Run each test in a temporary directory""" orig_dir = os.getcwd() temp_dir = tempfile.mkdtemp() os.chdir(temp_dir) try: yield temp_dir finally: os.chdir(orig_dir) shutil.rmtree(temp_dir) def test_load_no_configs() -> None: """No config files""" got = read_config() assert got is None @pytest.mark.parametrize( 'asset_name,expected', [ ('setup.cfg', [ ('requirements:Python 3', {'autoresolve': True}), ]), ('requirements.ini', [ ('requirements', {'allow_unsafe': True, 'use_cache': True, 'uv': True}), ]), ('pyproject.toml', [ ('one', {'uv': True}), ('two', {'generate_hashes': ['file.txt']}), ]), ] ) def test_load_single_config(asset_name: str, expected: List) -> None: """Load sample config file""" _write_asset(asset_name) got = read_config() assert got == expected def test_load_two_configs() -> None: """Combine setup.cfg and pyproject.toml""" _write_asset('setup.cfg') _write_asset('pyproject.toml') got = read_config() assert got == [ ('one', {'uv': True}), ('two', {'generate_hashes': ['file.txt']}), ('requirements:Python 3', {'autoresolve': True}) ] def test_load_ini_with_empty_pyproject() -> None: """Regression test - pyproject without requirements section""" _write_asset('setup.cfg') pathlib.Path('pyproject.toml').touch() got = read_config() assert got == [('requirements:Python 3', {'autoresolve': True})] def test_pyproject_without_section_name() -> None: """Regression test - pyproject without requirements section""" pathlib.Path('pyproject.toml').write_text( "[tool.requirements]\nuv=true\n", encoding="utf-8", ) got = read_config() assert got == [('config', {'uv': True})] ================================================ FILE: tests/test_conflicts.py ================================================ """End to end tests checking conflicts detection""" import pytest from click.testing import CliRunner from pipcompilemulti.cli_v1 import cli @pytest.mark.parametrize('conflict', ['merge', 'ref']) def test_conflict_detected(test_data_tmpdir, conflict): """Following types of version conflicts are detected: 1. Two files have different version and referenced from the third file. 2. File adds new constraint on package from referenced file. """ tmp_dir = test_data_tmpdir('conflicting-in-' + conflict) runner = CliRunner() result = runner.invoke( cli, ['--directory', str(tmp_dir)], ) assert result.exit_code == 1 assert ('Please add constraints' in str(result.exception) or 'Failed to pip-compile' in str(result.exception)) ================================================ FILE: tests/test_deduplicate.py ================================================ """Package name deduplication tests""" from pipcompilemulti.deduplicate import PackageDeduplicator def test_package_deduplicator_handles_delimiters_normalization(): """Tests minor package name variations are handled.""" package_deduplicator = PackageDeduplicator() package_deduplicator.on_discover([ {'in_path': 'a', 'refs': ['b']}, {'in_path': 'b', 'refs': []} ]) package_deduplicator.register_packages_for_env('b', {'pkg.name': '1.0'}) ignored_packages = package_deduplicator.ignored_packages('a') assert 'pkg-name' in ignored_packages assert 'pkg.name' in ignored_packages assert 'pkgname' not in ignored_packages assert ignored_packages['Pkg_Name'] == '1.0' ================================================ FILE: tests/test_dependency.py ================================================ """Dependency parser tests""" from pipcompilemulti.dependency import Dependency from pipcompilemulti.features import FEATURES from pipcompilemulti.options import OPTIONS def test_parse_package_name(): """Simple package name with a single "via" reference.""" dependency = Dependency("six==1.0 # via pkg") assert vars(dependency) == { "is_at": False, "is_vcs": False, "comment": " # via pkg", "comment_span": (8, 21), "hashes": "", "package": "six", "valid": True, "version": "1.0", "markers": "", "line": "six==1.0 # via pkg", } OPTIONS[FEATURES.skip_constraint_comments.OPTION_NAME] = True assert dependency.serialize() == "six==1.0 # via pkg" def test_parse_url_without_postfix(): """Package URL with package and constraint references.""" dependency = Dependency( "https://site.com/path#egg=dep\n # via\n # -c constraint\n # -r pkg" ) assert vars(dependency) == { "is_at": False, "is_vcs": True, "comment": "\n # via\n # -c constraint\n # -r pkg", "comment_span": (29, 66), "hashes": "", "line": ( "https://site.com/path#egg=dep\n" " # via\n" " # -c constraint\n" " # -r pkg" ), "package": "dep", "valid": True, "markers": "", "version": "", } def test_parse_url_with_postfix(): """VCS URL with package and constraint references.""" dependency = Dependency( "git+https://site@0.4.1#egg=dep==1.2.3_git&sub=dir" "\n # via\n # -c constraint\n # -r pkg" ) assert vars(dependency) == { "is_at": False, "is_vcs": True, "comment": "\n # via\n # -c constraint\n # -r pkg", "comment_span": (49, 86), "hashes": "", "line": ( "git+https://site@0.4.1#egg=dep==1.2.3_git&sub=dir\n" " # via\n" " # -c constraint\n" " # -r pkg" ), "package": "dep", "valid": True, "version": "", "markers": "", } OPTIONS[FEATURES.skip_constraint_comments.OPTION_NAME] = True assert dependency.serialize() == ( "git+https://site@0.4.1#egg=dep==1.2.3_git&sub=dir\n" " # via -r pkg" ) def test_parse_at_url_notation(): """Package URL with package and constraint references.""" dependency = Dependency( "dep @ https://site.com/path\n # via\n # -c constraint\n # -r pkg" ) assert vars(dependency) == { "is_at": True, "is_vcs": False, "comment": "\n # via\n # -c constraint\n # -r pkg", "comment_span": (27, 64), "hashes": "", "line": ( "dep @ https://site.com/path\n" " # via\n" " # -c constraint\n" " # -r pkg" ), "package": "dep", "valid": True, "version": "", "markers": "", } OPTIONS[FEATURES.skip_constraint_comments.OPTION_NAME] = True assert dependency.serialize() == ("dep @ https://site.com/path\n" " # via -r pkg") def test_sanitize_package_version(): """Simple package name with leading zeros in the version, with a single "via" reference.""" dependency = Dependency("gcsfs==2022.02.1 # via pkg") assert vars(dependency) == { "is_at": False, "is_vcs": False, "comment": " # via pkg", "comment_span": (16, 29), "hashes": "", "package": "gcsfs", "valid": True, "version": "2022.2.1", "markers": "", "line": "gcsfs==2022.02.1 # via pkg", } OPTIONS[FEATURES.skip_constraint_comments.OPTION_NAME] = True assert dependency.serialize() == "gcsfs==2022.2.1 # via pkg" def test_parse_sys_platform(): """Package URL with package and constraint references.""" dependency = Dependency('dep==1 ; sys_platform == "darwin" # Comment') assert vars(dependency) == { "line": 'dep==1 ; sys_platform == "darwin" # Comment', "valid": True, "is_at": False, "is_vcs": False, "package": "dep", "version": "1", "markers": ' ; sys_platform == "darwin"', "hashes": "", "comment": " # Comment", "comment_span": (33, 43), } OPTIONS[FEATURES.skip_constraint_comments.OPTION_NAME] = True assert dependency.serialize() == ('dep==1 ; sys_platform == "darwin" # Comment') ================================================ FILE: tests/test_discover.py ================================================ """Environment discovery tests.""" import os import sys import pytest from pipcompilemulti.discover import discover @pytest.mark.skipif(sys.platform == "win32", reason="Path normalization is wonky under Windows") def test_discover_nested(): """Test references to other dirs are discovered.""" envs = discover(os.path.join("nested", "*.in")) assert envs == [ { "in_path": os.path.join("nested", "up.in"), "name": "up", "refs": set(), }, { "in_path": os.path.join("nested", "subproject", "base.in"), "name": "base", "refs": {os.path.join("..", "up.in")}, }, { "in_path": os.path.join("nested", "subproject", "sub.in"), "name": "sub", "refs": {"base.in"}, }, { "in_path": os.path.join("nested", "base.in"), "name": "base", "refs": {os.path.join("subproject", "sub.in")}, }, { "in_path": os.path.join("nested", "diamond.in"), "name": "diamond", "refs": {"base.in", os.path.join("subproject", "base.in")}, }, ] ================================================ FILE: tests/test_pipcompilemulti.py ================================================ """Tests for pip-compile-multi""" import os try: from unittest import mock except ImportError: import mock import pytest from pipcompilemulti.environment import Environment from pipcompilemulti.dependency import Dependency from pipcompilemulti.options import OPTIONS from pipcompilemulti.deduplicate import PackageDeduplicator from pipcompilemulti.utils import merged_packages, reference_cluster from pipcompilemulti.features.header import DEFAULT_HEADER PIN = 'pycodestyle==2.3.1 # via flake8' CMPT = 'pycodestyle~=2.3.1 # via flake8' def test_fix_compatible_pin(): """Test == is replaced with ~= for compatible dependencies""" env = Environment('xxx') with mock.patch.dict(OPTIONS, {'compatible_patterns': ['pycode*']}): result = env.fix_pin(PIN) assert result == CMPT def test_no_fix_incompatible_pin(): """Test dependency is left unchanged be default""" env = Environment('') result = env.fix_pin(PIN) assert result == PIN def test_pin_is_ommitted_if_set_to_ignore(): """Test ignored files won't pass""" dedup = PackageDeduplicator() dedup.on_discover([ {'in_path': 'a', 'refs': ['b']}, {'in_path': 'b', 'refs': []} ]) dedup.register_packages_for_env('b', {'pycodestyle': '2.3.1'}) env = Environment('a', deduplicator=dedup) result = env.fix_pin(PIN) assert result is None def test_post_releases_are_kept_by_default(): """Test postXXX versions are truncated to release""" pin = 'pycodestyle==2.3.1.post2231 # via flake8' env = Environment('') result = env.fix_pin(pin) assert result == pin def test_forbid_post_releases(): """Test postXXX versions are kept if allow_post=True""" pin = 'pycodestyle==2.3.1.post2231 # via flake8' with mock.patch.dict(OPTIONS, {'forbid_post': ['env']}): env = Environment('env') result = env.fix_pin(pin) assert result == PIN @pytest.mark.parametrize('in_path, refs', [ ('base.in', set()), ('test.in', {'base.in'}), ('local.in', {'test.in'}), ]) def test_parse_references(in_path, refs): """Check references are parsed for sample files""" env = Environment('') result = env.parse_references( os.path.join('requirements', in_path) ) assert result == refs def test_split_header(): """Check that default header is parsed from autogenerated base.txt""" with open(os.path.join('requirements', 'base.txt'), encoding="utf-8") as fp: header, _ = Environment.split_header(fp) expected = [ line + '\n' for line in DEFAULT_HEADER.splitlines() ] assert header[1:] == expected def test_concatenation(): """Check lines are joined and extra spaces removed""" lines = Environment.concatenated([ 'abc \\\n', ' 123 \\\n', '?\n', 'MMM\n', ]) assert list(lines) == ['abc 123 ?', 'MMM'] def test_parse_hashes_with_comment(): """Check that sample is parsed""" dep = Dependency( 'lib==ver --hash=123 --hash=abc # comment' ) assert dep.hashes == '--hash=123 --hash=abc' def test_parse_hashes_without_comment(): """Check that sample is parsed""" dep = Dependency( 'lib==ver --hash=123 --hash=abc' ) assert dep.valid assert dep.hashes == '--hash=123 --hash=abc' def test_serialize_hashes(): """Check serialization in pip-tools style""" dep = Dependency( 'lib==ver --hash=123 --hash=abc # comment' ) text = dep.serialize() assert text == ( "lib==ver \\\n" " --hash=123 \\\n" " --hash=abc # comment" ) def test_reference_cluster(): """Check cluster propagets both ways""" for entry in ['base', 'test', 'local', 'doc']: cluster = reference_cluster([ {'in_path': 'base', 'refs': []}, {'in_path': 'test', 'refs': ['base']}, {'in_path': 'local', 'refs': ['test']}, {'in_path': 'doc', 'refs': ['base']}, {'in_path': 'side', 'refs': []}, ], entry) assert cluster == set(['base', 'doc', 'local', 'test']) def test_parse_vcs_dependencies(): """ Check VCS support https://pip.pypa.io/en/stable/reference/pip_install/#vcs-support """ cases = ( "git://git.myproject.org/MyProject#egg=MyProject", "-e git://git.myproject.org/MyProject#egg=MyProject", "git+http://git.myproject.org/MyProject#egg=MyProject", "-e git+http://git.myproject.org/MyProject#egg=MyProject", "git+https://git.myproject.org/MyProject#egg=MyProject", "-e git+https://git.myproject.org/MyProject#egg=MyProject", "git+ssh://git.myproject.org/MyProject#egg=MyProject", "-e git+ssh://git.myproject.org/MyProject#egg=MyProject", "git+git://git.myproject.org/MyProject#egg=MyProject", "-e git+git://git.myproject.org/MyProject#egg=MyProject", "git+file://git.myproject.org/MyProject#egg=MyProject", "-e git+file://git.myproject.org/MyProject#egg=MyProject", "-e git+git@git.myproject.org:MyProject#egg=MyProject", # Passing branch names, a commit hash or a tag name is possible like so: "git://git.myproject.org/MyProject.git@master#egg=MyProject", "-e git://git.myproject.org/MyProject.git@master#egg=MyProject", "git://git.myproject.org/MyProject.git@v1.0#egg=MyProject", "-e git://git.myproject.org/MyProject.git@v1.0#egg=MyProject", "git://git.myproject.org/MyProject.git@" "da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject", "-e git://git.myproject.org/MyProject.git@" "da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject", # Mercurial "hg+http://hg.myproject.org/MyProject#egg=MyProject", "-e hg+http://hg.myproject.org/MyProject#egg=MyProject", "hg+https://hg.myproject.org/MyProject#egg=MyProject", "-e hg+https://hg.myproject.org/MyProject#egg=MyProject", "hg+ssh://hg.myproject.org/MyProject#egg=MyProject", "-e hg+ssh://hg.myproject.org/MyProject#egg=MyProject", # You can also specify a revision number, a revision hash, # a tag name or a local branch name like so: "hg+http://hg.myproject.org/MyProject@da39a3ee5e6b#egg=MyProject", "-e hg+http://hg.myproject.org/MyProject@da39a3ee5e6b#egg=MyProject", "hg+http://hg.myproject.org/MyProject@2019#egg=MyProject", "-e hg+http://hg.myproject.org/MyProject@2019#egg=MyProject", "hg+http://hg.myproject.org/MyProject@v1.0#egg=MyProject", "-e hg+http://hg.myproject.org/MyProject@v1.0#egg=MyProject", "hg+http://hg.myproject.org/MyProject@special_feature#egg=MyProject", "-e hg+http://hg.myproject.org/MyProject@special_feature#egg=MyProject", # Subversion "svn+svn://svn.myproject.org/svn/MyProject#egg=MyProject", "-e svn+svn://svn.myproject.org/svn/MyProject#egg=MyProject", "svn+http://svn.myproject.org/svn/MyProject/trunk@2019#egg=MyProject", "-e svn+http://svn.myproject.org/svn/MyProject/trunk@2019#egg=MyProject", # Bazaar "bzr+http://bzr.myproject.org/MyProject/trunk#egg=MyProject", "-e bzr+http://bzr.myproject.org/MyProject/trunk#egg=MyProject", "bzr+sftp://user@myproject.org/MyProject/trunk#egg=MyProject", "-e bzr+sftp://user@myproject.org/MyProject/trunk#egg=MyProject", "bzr+ssh://user@myproject.org/MyProject/trunk#egg=MyProject", "-e bzr+ssh://user@myproject.org/MyProject/trunk#egg=MyProject", "bzr+ftp://user@myproject.org/MyProject/trunk#egg=MyProject", "-e bzr+ftp://user@myproject.org/MyProject/trunk#egg=MyProject", "bzr+lp:MyProject#egg=MyProject", "-e bzr+lp:MyProject#egg=MyProject", # Tags or revisions can be installed like so: "bzr+https://bzr.myproject.org/MyProject/trunk@2019#egg=MyProject", "-e bzr+https://bzr.myproject.org/MyProject/trunk@2019#egg=MyProject", "bzr+http://bzr.myproject.org/MyProject/trunk@v1.0#egg=MyProject", "-e bzr+http://bzr.myproject.org/MyProject/trunk@v1.0#egg=MyProject", # Zulip "-e git+https://github.com/zulip/talon.git@" "7d8bdc4dbcfcc5a73298747293b99fe53da55315#egg=talon==1.2.10.zulip1", "-e git+https://github.com/zulip/ultrajson@70ac02bec#egg=ujson==1.35+git", "-e git+https://github.com/zulip/virtualenv-clone.git@" "44e831da39ffb6b9bb5c7d103d98babccdca0456#egg=virtualenv-clone==0.2.6.zulip1", '-e "git+https://github.com/zulip/python-zulip-api.git@' '0.4.1#egg=zulip==0.4.1_git&subdirectory=zulip"', '-e "git+https://github.com/zulip/python-zulip-api.git@' '0.4.1#egg=zulip_bots==0.4.1+git&subdirectory=zulip_bots"', # AWX: "-e git+https://github.com/ansible/ansiconv.git@tower_1.0.0#egg=ansiconv", "-e git+https://github.com/ansible/django-qsstats-magic.git@" "tower_0.7.2#egg=django-qsstats-magic", "-e git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding", "-e git+https://github.com/ansible/django-jsonbfield@" "fix-sqlite_serialization#egg=jsonbfield", "-e git+https://github.com/ansible/docutils.git@master#egg=docutils", ) for line in cases: dependency = Dependency(line) assert dependency.valid, line serialized = dependency.serialize() if line.startswith('-e') and 'git+git@' not in line: expect = line.split(' ', 1)[1] else: expect = line assert serialized == expect def test_merged_packages_raise_for_conflict(): """Check that package x can't be locked to versions 1 and 2""" with pytest.raises(RuntimeError): merged_packages( { 'a': {'x': 1}, 'b': {'x': 2}, }, ['a', 'b'] ) def test_fix_pin_detects_version_conflict(): """Check that package x can't be locked to versions 1 and 2""" dedup = PackageDeduplicator() dedup.on_discover([ {'in_path': 'a', 'refs': ['b']}, {'in_path': 'b', 'refs': []} ]) dedup.register_packages_for_env('b', {'x': '1'}) env = Environment('a', deduplicator=dedup) ignored_pin = env.fix_pin('x==1') assert ignored_pin is None with pytest.raises(RuntimeError): env.fix_pin('x==2') ================================================ FILE: tests/test_skip_constraint_comments.py ================================================ """Tests for Skip constraints in comments of output files feature.""" from textwrap import dedent from pipcompilemulti.features.skip_constraint_comments import SkipConstraintComments _SOURCE = dedent( """ # via # -c path/to/sink.txt # -r path/to/requirements.in """ ).rstrip() _EXPECTED = dedent( """ # via -r path/to/requirements.in """ ).rstrip() class SkipConstraintCommentsAlwayOn(SkipConstraintComments): """Force-enabled feature.""" enabled = True def test_drop_sink_example(): """Drops sink comment when using package @ url notation.""" feature = SkipConstraintCommentsAlwayOn() result = feature.process_dependency_comments(_SOURCE) assert result == _EXPECTED ================================================ FILE: tests/test_upgrade_feature.py ================================================ """Test upgrade feature.""" from pipcompilemulti.features import FEATURES from pipcompilemulti.options import OPTIONS def test_upgrade_package_disables_upgrade(): """Even if --update is passed, --upgrade-package disables it.""" OPTIONS.update({ 'upgrade': True, 'upgrade_packages': ['a'], }) assert not FEATURES.upgrade_all.enabled ================================================ FILE: tests/test_utils.py ================================================ """Utils tests.""" import sys import pytest from pipcompilemulti.utils import recursive_refs @pytest.mark.skipif(sys.platform == "win32", reason="Pass normalization is wonky under Windows") def test_recursive_refs(): """Test sample inputs.""" result = sorted(recursive_refs([ {'in_path': 'base.in', 'refs': []}, {'in_path': 'sub/test.in', 'refs': ['../base.in']}, {'in_path': 'local.in', 'refs': ['sub/test.in']}, ], 'local.in')) assert result == ['base.in', 'sub/test.in'] result = sorted(recursive_refs([ {'in_path': 'base.in', 'refs': []}, {'in_path': 'sub/test.in', 'refs': ['../base.in']}, {'in_path': 'local.in', 'refs': ['sub/test.in']}, ], 'sub/test.in')) assert result == ['base.in'] ================================================ FILE: tests/upgrade/base.in ================================================ # Set an upper bound for test stability markupsafe<2.1.3 # Another package which shouldn't be upgraded wheel ================================================ FILE: tests/upgrade/base.txt ================================================ # SHA1:361e74487f9a6061b75ff3372ad729f74dcfd875 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # markupsafe==2.0.1 # via -r tests/upgrade/base.in wheel==0.29.0 # via -r tests/upgrade/base.in ================================================ FILE: tests/upgrade-autoresolve-with-range/base.in ================================================ markupsafe ================================================ FILE: tests/upgrade-autoresolve-with-range/base.txt ================================================ # SHA1:2f781f20c9698050fd9afac72c0364db5deacb05 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # markupsafe==2.0.1 # via # -c tests/upgrade-autoresolve-with-range/prod.txt # -r tests/upgrade-autoresolve-with-range/base.in ================================================ FILE: tests/upgrade-autoresolve-with-range/prod.in ================================================ -r base.in ================================================ FILE: tests/upgrade-autoresolve-with-range/prod.txt ================================================ # SHA1:a87fd594461015e819a1f468943967d12880b85d # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r base.txt ================================================ FILE: tests/upgrade-autoresolve-with-range-expected/base.in ================================================ markupsafe ================================================ FILE: tests/upgrade-autoresolve-with-range-expected/base.txt ================================================ # SHA1:2f781f20c9698050fd9afac72c0364db5deacb05 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # markupsafe==2.1.1 # via -r tests/upgrade-autoresolve-with-range-expected/base.in ================================================ FILE: tests/upgrade-autoresolve-with-range-expected/prod.in ================================================ -r base.in ================================================ FILE: tests/upgrade-autoresolve-with-range-expected/prod.txt ================================================ # SHA1:a87fd594461015e819a1f468943967d12880b85d # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # -r base.txt ================================================ FILE: tests/upgrade-expected/base.in ================================================ # Set an upper bound for test stability markupsafe<2.1.3 # Another package which shouldn't be upgraded wheel ================================================ FILE: tests/upgrade-expected/base.txt ================================================ # SHA1:361e74487f9a6061b75ff3372ad729f74dcfd875 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # markupsafe==2.1.2 # via -r tests/upgrade-expected/base.in wheel==0.29.0 # via -r tests/upgrade-expected/base.in ================================================ FILE: tests/upgrade-with-range/base.in ================================================ markupsafe ================================================ FILE: tests/upgrade-with-range/base.txt ================================================ # SHA1:2f781f20c9698050fd9afac72c0364db5deacb05 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # markupsafe==2.1.1 # via -r tests/upgrade-with-range/base.in ================================================ FILE: tests/upgrade-with-range-expected/base.in ================================================ markupsafe ================================================ FILE: tests/upgrade-with-range-expected/base.txt ================================================ # SHA1:2f781f20c9698050fd9afac72c0364db5deacb05 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # markupsafe==2.1.1 # via -r tests/upgrade-with-range-expected/base.in ================================================ FILE: tests/utils.py ================================================ """Test utilities.""" import os import tempfile import shutil import contextlib @contextlib.contextmanager def temp_dir(): """Create temporary directory with copy of requirements.""" tmp_dir = tempfile.mkdtemp() os.rmdir(tmp_dir) shutil.copytree('requirements', os.path.join(tmp_dir, 'requirements')) shutil.copy('setup.cfg', tmp_dir) old_cwd = os.getcwd() os.chdir(tmp_dir) yield tmp_dir os.chdir(old_cwd) shutil.rmtree(tmp_dir) ================================================ FILE: tox.ini ================================================ [tox] envlist = py{39,310,311,312,313,314,py3}-{linux,windows,darwin}, lint, checkdocs, verify skip_missing_interpreters = true [testenv] platform = linux: linux windows: win32 darwin: darwin commands = python -m pytest --cov=pipcompilemulti --cov-branch --cov-report=html deps = linux: -r{toxinidir}/requirements/test.hash darwin: -r{toxinidir}/requirements/test.hash windows: -r{toxinidir}/requirements/testwin.hash [testenv:lint] skip_install = true basepython = python3.10 commands = pylint -r y pipcompilemulti/ tests/ deps = -r{toxinidir}/requirements/local.hash [testenv:checkdocs] skip_install = true commands = python setup.py checkdocs deps = collective.checkdocs pygments [testenv:verify] skipsdist = true skip_install = true basepython = python3.10 deps = pip-compile-multi commands = pip-compile-multi verify whitelist_externals = pip-compile-multi [testenv:lock] basepython = python3.10 usedevelop = True deps = -rrequirements/test.hash commands = requirements lock [testenv:upgrade] basepython = python3.10 usedevelop = True deps = -rrequirements/test.hash commands = requirements upgrade [pytest] addopts = -vvvs --doctest-modules log_cli = true log_level = NOTSET