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