pax_global_header00006660000000000000000000000064145117464110014516gustar00rootroot0000000000000052 comment=380836a88496ebf6e3006a4eb2005185c41b3204 scriv-1.4.0/000077500000000000000000000000001451174641100126465ustar00rootroot00000000000000scriv-1.4.0/.editorconfig000066400000000000000000000007551451174641100153320ustar00rootroot00000000000000[*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 4 max_line_length = 80 trim_trailing_whitespace = true [{Makefile, *.mk}] indent_style = tab indent_size = 8 [*.{yml,yaml,json}] indent_size = 2 [*.js] indent_size = 2 [*.diff] trim_trailing_whitespace = false [.git/*] trim_trailing_whitespace = false [*.rst] max_line_length = 79 [requirements/*.{in,txt}] # No idea why, but pip-tools puts comments on 2-space indents. indent_size = 2 scriv-1.4.0/.github/000077500000000000000000000000001451174641100142065ustar00rootroot00000000000000scriv-1.4.0/.github/FUNDING.yml000066400000000000000000000000171451174641100160210ustar00rootroot00000000000000github: nedbat scriv-1.4.0/.github/dependabot.yml000066400000000000000000000006011451174641100170330ustar00rootroot00000000000000# From: # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/keeping-your-actions-up-to-date-with-dependabot # Set update schedule for GitHub Actions version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions once a week interval: "weekly" scriv-1.4.0/.github/workflows/000077500000000000000000000000001451174641100162435ustar00rootroot00000000000000scriv-1.4.0/.github/workflows/tests.yml000066400000000000000000000121761451174641100201370ustar00rootroot00000000000000# Run scriv CI name: "Test Suite" on: push: pull_request: workflow_dispatch: defaults: run: shell: bash concurrency: group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: PIP_DISABLE_PIP_VERSION_CHECK: 1 PANDOC_VER: 2.18 jobs: tests: name: "Test on ${{ matrix.os }}" runs-on: "${{ matrix.os }}-latest" strategy: fail-fast: false matrix: os: - ubuntu - macos - windows steps: - name: "Check out the repo" uses: "actions/checkout@v4" - name: "Set up Python" id: "setup-python" uses: "actions/setup-python@v4" with: # The last listed Python version is the default. python-version: | pypy-3.9 3.7 3.8 3.9 3.10 3.11 3.12 - name: "Restore cache" id: "restore-cache" uses: "actions/cache@v3" with: path: | .tox/ .venv/ key: "cache-python-${{ steps.setup-python.outputs.python-version }}-os-${{ runner.os }}-hash-${{ hashFiles('tox.ini', 'requirements/*.txt') }}" - name: "Identify venv path" shell: "bash" run: "echo 'venv-path=${{ runner.os == 'Windows' && '.venv/Scripts' || '.venv/bin' }}' >> $GITHUB_ENV" - name: "Install dependencies" if: "steps.restore-cache.outputs.cache-hit == false" run: | python -m venv .venv ${{ env.venv-path }}/python -m pip install -U setuptools ${{ env.venv-path }}/python -m pip install -r requirements/tox.txt - name: "Install pandoc on Linux" # sudo apt-get pandoc: will install a version from 2018! if: runner.os == 'Linux' run: | wget -nv -O pandoc.deb https://github.com/jgm/pandoc/releases/download/${PANDOC_VER}/pandoc-${PANDOC_VER}-1-amd64.deb sudo apt install ./pandoc.deb - name: "Install pandoc on Mac" if: runner.os == 'macOS' run: | brew install pandoc - name: "Install pandoc on Windows" if: runner.os == 'Windows' run: | choco install -y -r --no-progress pandoc - name: "Run tox" run: | ${{ env.venv-path }}/python -m tox -m ci-tests - name: "Upload coverage data" uses: actions/upload-artifact@v3 with: name: covdata path: .coverage.* coverage: name: Coverage needs: tests runs-on: ubuntu-latest steps: - name: "Check out the repo" uses: "actions/checkout@v4" - name: "Set up Python" uses: "actions/setup-python@v4" with: python-version: "3.12" cache: pip cache-dependency-path: 'requirements/*.txt' - name: "Install dependencies" run: | python -m pip install -U setuptools python -m pip install -r requirements/tox.txt - name: "Download coverage data" uses: actions/download-artifact@v3 with: name: covdata - name: "Combine and report" run: | python -m tox -e coverage export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") echo "total=$TOTAL" >> $GITHUB_ENV echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY - name: "Make badge" if: (github.repository == 'nedbat/scriv') && (github.ref == 'refs/heads/main') # https://gist.github.com/nedbat/5a304c1c779d4bcc57be95f847e9327f uses: schneegans/dynamic-badges-action@v1.6.0 with: # GIST_TOKEN is a GitHub personal access token with scope "gist". # https://github.com/settings/tokens/969369418 auth: ${{ secrets.GIST_TOKEN }} gistID: 5a304c1c779d4bcc57be95f847e9327f filename: covbadge.json label: Coverage message: ${{ env.total }}% minColorRange: 50 maxColorRange: 90 valColorRange: ${{ env.total }} docs: name: Docs runs-on: ubuntu-latest steps: - name: "Check out the repo" uses: "actions/checkout@v4" - name: "Set up Python" uses: "actions/setup-python@v4" with: python-version: "3.7" cache: pip cache-dependency-path: 'requirements/*.txt' - name: "Install dependencies" run: | python -m pip install -U setuptools python -m pip install -r requirements/tox.txt - name: "Build docs" run: | python -m tox -e docs quality: name: Linters etc runs-on: ubuntu-latest steps: - name: "Check out the repo" uses: "actions/checkout@v4" - name: "Set up Python" uses: "actions/setup-python@v4" with: python-version: "3.7" cache: pip cache-dependency-path: 'requirements/*.txt' - name: "Install dependencies" run: | python -m pip install -U setuptools python -m pip install -r requirements/tox.txt - name: "Linters etc" run: | python -m tox -e quality scriv-1.4.0/.gitignore000066400000000000000000000012001451174641100146270ustar00rootroot00000000000000*.py[cod] __pycache__ .pytest_cache .mypy_cache # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .cache/ .pytest_cache/ .coverage .coverage.* .tox coverage.json coverage.xml htmlcov/ # IDEs and text editors *~ *.swp .idea/ .project .pycharm_helpers/ .pydevproject # The Silver Searcher .agignore # OS X artifacts *.DS_Store # Logging log/ logs/ chromedriver.log ghostdriver.log # Complexity output/*.html output/*/index.html # Sphinx docs/_build docs/modules.rst docs/journo.rst docs/journo.*.rst scriv-1.4.0/CHANGELOG.rst000066400000000000000000000376131451174641100147010ustar00rootroot00000000000000.. this will be appended to README.rst Changelog ========= .. All enhancements and patches to scriv will be documented in this file. It adheres to the structure of http://keepachangelog.com/ , but in reStructuredText instead of Markdown (for ease of incorporation into Sphinx documentation and the PyPI description). This project adheres to Semantic Versioning (http://semver.org/). Unreleased ---------- See the fragment files in the `changelog.d directory`_. .. _changelog.d directory: https://github.com/nedbat/scriv/tree/master/changelog.d .. scriv-insert-here .. _changelog-1.4.0: 1.4.0 — 2023-10-12 ------------------ Added ..... - Literals can be extracted from .cabal files. Thanks `Javier Sagredo `_. - Use the git config ``scriv.user_nick`` for the user nick part of the fragment file. Thanks to `Ronny Pfannschmidt `_, fixing `issue 103`_. - Settings can now be prefixed with ``command:`` to execute the rest of the setting as a shell command. The output of the command will be used as the value of the setting. Fixed ..... - If there are no changelog fragments, ``scriv collect`` now exits with status code of 2, fixing `issue 110`_. - Changelogs with non-version headings now produce an understandable error message from ``scriv collect``, thanks to `James Gerity `_, fixing `issue 100`_. .. _pull 91: https://github.com/nedbat/scriv/pull/91 .. _issue 100: https://github.com/nedbat/scriv/issues/100 .. _pull 101: https://github.com/nedbat/scriv/pull/101 .. _issue 103: https://github.com/nedbat/scriv/pull/103 .. _pull 106: https://github.com/nedbat/scriv/pull/106 .. _issue 110: https://github.com/nedbat/scriv/issues/110 .. _changelog-1.3.1: 1.3.1 — 2023-04-16 ------------------ Fixed ..... - The Version class introduced in 1.3.0 broke the ``scriv github-release`` command. This is now fixed. .. _changelog-1.3.0: 1.3.0 — 2023-04-16 ------------------ Added ..... - ``.cfg`` files can now be read with ``literal:`` settings, thanks to `Matias Guijarro `_. .. _pull 88: https://github.com/nedbat/scriv/pull/88 Fixed ..... - In compliance with `PEP 440`_, comparing version numbers now ignores a leading "v" character. This makes scriv more flexible about how you present version numbers in various places (code literals, changelog entries, git tags, and so on). Fixes `issue 89`_. .. _PEP 440: https://peps.python.org/pep-0440/ .. _issue 89: https://github.com/nedbat/scriv/issues/89 .. _changelog-1.2.1: 1.2.1 — 2023-02-18 ------------------ Fixed ..... - Scriv would fail trying to import tomllib on Python <3.11 if installed without the ``[toml]`` extra. This is now fixed, closing `issue 80`_. - Settings specified as ``file:`` will now search in the changelog directory and then the current directory for the file. The only exception is if the first component is ``.`` or ``..``, then only the current directory is considered. Fixes `issue 82`_. - Python variables with type annotations can now be read with ``literal:`` settings, fixing `issue 85`_. - Error messages for mis-formed ``literal:`` configuration values are more precise, as requested in `issue 84`_. - Error messages from settings validation are ScrivExceptions now, and report configuration problems more clearly and earlier in some cases. .. _issue 80: https://github.com/nedbat/scriv/issues/80 .. _issue 82: https://github.com/nedbat/scriv/issues/82 .. _issue 84: https://github.com/nedbat/scriv/issues/84 .. _issue 85: https://github.com/nedbat/scriv/issues/85 .. _changelog-1.2.0: 1.2.0 — 2023-01-18 ------------------ Added ..... - ``scriv github-release`` now has a ``--repo=`` option to specify which GitHub repo to use when you have multiple remotes. Changed ....... - Improved the error messages from ``scriv github-release`` when a GitHub repo can't be identified among the git remotes. .. _changelog-1.1.0: 1.1.0 — 2023-01-16 ------------------ Added ..... - The ``scriv github-release`` command has a new setting, ``ghrel_template``. This is a template to use when building the release text, to add text before or after the Markdown extracted from the changelog. - The ``scriv github-release`` command now has a ``--dry-run`` option to show what would happen, without posting to GitHub. Changed ....... - File names specified for ``file:`` settings will be interpreted relative to the current directory if they have path components. If the file name has no slashes or backslashes, then the old behavior remains: the file will be found in the fragment directory, or as a built-in template. - All exceptions raised by Scriv are now ScrivException. Fixed ..... - Parsing changelogs now take the `insert-marker` setting into account. Only content after the insert-marker line is parsed. - More internal activities are logged, to help debug operations. .. _changelog-1.0.0: 1.0.0 — 2022-12-03 ------------------ Added ..... - Now literal configuration settings can be read from YAML files. Closes `issue 69`_. Thanks, `Florian Küpper `_. .. _pull 70: https://github.com/nedbat/scriv/pull/70 .. _issue 69: https://github.com/nedbat/scriv/issues/69 Fixed ..... - Fixed truncated help summaries by shortening them, closing `issue 63`_. .. _issue 63: https://github.com/nedbat/scriv/issues/63 .. _changelog-0.17.0: 0.17.0 — 2022-09-18 ------------------- Added ..... - The ``collect`` command now has a ``--title=TEXT`` option to provide the exact text to use as the title of the new changelog entry. Finishes `issue 48`_. .. _issue 48: https://github.com/nedbat/scriv/issues/48 Changed ....... - The ``github_release`` command now only considers the top-most entry in the changelog. You can use the ``--all`` option to continue the old behavior of making or updating GitHub releases for all of the entries. This change makes it easier for projects to start using scriv with an existing populated changelog file. Closes `issue 57`_. .. _issue 57: https://github.com/nedbat/scriv/issues/57 Fixed ..... - If there were no fragments to collect, `scriv collect` would make a new empty section in the changelog. This was wrong, and is now fixed. Now the changelog remains unchanged in this case. Closes `issue 55`_. .. _issue 55: https://github.com/nedbat/scriv/issues/55 - The ``github-release`` command will now issue a warning for changelog entries that have no version number. These can't be made into releases, so they are skipped. (`issue 56`_). .. _issue 56: https://github.com/nedbat/scriv/issues/56 - ``scriv collect`` will end with an error now if the version number would duplicate a version number on an existing changelog entry. Fixes `issue 26`_. .. _issue 26: https://github.com/nedbat/scriv/issues/26 .. _changelog-0.16.0: 0.16.0 — 2022-07-24 ------------------- Added ..... - The ``github_release`` command will use a GitHub personal access token stored in the GITHUB_TOKEN environment variable, or from a .netrc file. Fixed ..... - The github_release command was using `git tags` as a command when it should have used `git tag`. - Anchors in the changelog were being included in the previous sections when creating GitHub releases. This has been fixed, closing `issue 53`_. .. _issue 53: https://github.com/nedbat/scriv/issues/53 .. _changelog-0.15.2: 0.15.2 — 2022-06-18 ------------------- Fixed ..... - Quoted commands failed, so we couldn't determine the GitHub remote. .. _changelog-0.15.1: 0.15.1 — 2022-06-18 ------------------- Added ..... - Added docs for ``scriv github-release``. Fixed ..... - Call pandoc properly on Windows for the github_release command. .. _changelog-0.15.0: 0.15.0 — 2022-04-24 ------------------- Removed ....... - Dropped support for Python 3.6. Added ..... - The `github-release` command parses the changelog and creates GitHub releases from the entries. Changed entries will update the corresponding release. - Added a ``--version`` option. Changed ....... - Parsing of fragments now only attends to the top-level section headers, and includes nested headers instead of splitting on all headers. .. _changelog-0.14.0: 0.14.0 — 2022-03-23 ------------------- Added ..... - Add an anchor before each version section in the output of ``scriv collect`` so URLs for the sections are predictable and stable for each new version (Fixes `issue 46`_). Thanks Abhilash Raj and Rodrigo Girão Serrão. Fixed ..... - Markdown fragments weren't combined properly. Now they are. Thanks Rodrigo Girão Serrão. .. _issue 46: https://github.com/nedbat/scriv/issues/46 0.13.0 — 2022-01-23 ------------------- Added ..... - Support finding version information in TOML files (like ``pyproject.toml``) using the ``literal`` configuration directive. Thanks, Kurt McKee 0.12.0 — 2021-07-28 ------------------- Added ..... - Fragment files in the fragment directory will be skipped if they match the new configuration value ``skip_fragments``, a glob pattern. The default value is "README.*". This lets you put a README.md file in that directory to explain its purpose, as requested in `issue 40`_. .. _issue 40: https://github.com/nedbat/scriv/issues/40 Changed ....... - Switched from "toml" to "tomli" for reading TOML files. Fixed ..... - Setting ``format=md`` didn't properly cascade into other default settings, leaving you with RST settings that needed to be explicitly overridden (`issue 39`_). This is now fixed. .. _issue 39: https://github.com/nedbat/scriv/issues/39 0.11.0 — 2021-06-22 ------------------- Added ..... - A new poorly documented API is available. See the Scriv, Changelog, and Fragment classes in the scriv.scriv module. Changed ....... - Python 3.6 is now the minimum supported Python version. Fixed ..... - The changelog is now always written as UTF-8, regardless of the default encoding of the system. Thanks, Hei (yhlam). 0.10.0 — 2020-12-27 ------------------- Added ..... - Settings can now be read from a pyproject.toml file. Install with the "[toml]" extra to be sure TOML support is available. Closes `issue 9`_. .. _issue 9: https://github.com/nedbat/scriv/issues/9 - Added the Philosophy section of the docs. Changed ....... - The default entry header no longer puts the version number in square brackets: this was a misunderstanding of the keepachangelog formatting. - Respect the existing newline style of changelog files. (`#14`_) This means that a changelog file with Linux newlines on a Windows platform will be updated with Linux newlines, not rewritten with Windows newlines. Thanks, Kurt McKee. .. _#14: https://github.com/nedbat/scriv/issues/14 Fixed ..... - Support Windows' directory separator (``\``) in unit test output. (`#15`_) This allows the unit tests to run in Windows environments. Thanks, Kurt McKee. - Explicitly specify the directories and files that Black should scan. (`#15`_) This prevents Black from scanning every file in a virtual environment. Thanks, Kurt McKee. - Using "literal:" values in the configuration file didn't work on Python 3.6 or 3.7, as reported in `issue 18`_. This is now fixed. .. _#15: https://github.com/nedbat/scriv/issues/15 .. _issue 18: https://github.com/nedbat/scriv/issues/18 0.9.2 — 2020-08-29 ------------------ - Packaging fix. 0.9.0 — 2020-08-29 ------------------ Added ..... - Markdown format is supported, both for fragments and changelog entries. - Fragments can be mixed (some .rst and some .md). They will be collected and output in the format configured in the settings. - Documentation. - "python -m scriv" now works. Changed ....... - The version number is displayed in the help message. 0.8.1 — 2020-08-09 ------------------ Added ..... - When editing a new fragment during "scriv create", if the edited fragment has no content (only comments or blank lines), then the create operation will be aborted, and the file will be removed. (Closes `issue 2`_.) .. _issue 2: https://github.com/nedbat/scriv/issues/2 Changed ....... - If the fragment directory doesn't exist, a simple direct message is shown, rather than a misleading FileNotFound error (closes `issue 1`_). .. _issue 1: https://github.com/nedbat/scriv/issues/1 Fixed ..... - When not using categories, comments in fragment files would be copied to the changelog file (`issue 3`_). This is now fixed. .. _issue 3: https://github.com/nedbat/scriv/issues/3 - RST syntax is better understood, so that hyperlink references and directives will be preserved. Previously, they were mistakenly interpreted as comments and discarded. 0.8.0 — 2020-08-04 ------------------ Added ..... - Added the `collect` command. - Configuration is now read from setup.cfg or tox.ini. - A new configuration setting, rst_section_char, determines the character used in the underlines for the section headings in .rst files. - The `new_entry_template` configuration setting is the name of the template file to use when creating new entries. The file will be found in the `fragment_directory` directory. The file name defaults to ``new_entry.FMT.j2``. If the file doesn't exist, an internal default will be used. - Now the collect command also includes a header for the entire entry. The underline is determined by the "rst_header_char" settings. The heading text is determined by the "header" setting, which defaults to the current date. - The categories list in the config can be empty, meaning entries are not categorized. - The create command now accepts --edit (to open the new entry in your text editor), and --add (to "git add" the new entry). - The collect command now accepts --edit (to open the changelog file in an editor after the new entries have been collected) and --add (to git-add the changelog file and git rm the entries). - The names of the main git branches are configurable as "main_branches" in the configuration file. The default is "master", "main", and "develop". - Configuration values can now be read from files by prefixing them with "file:". File names will be interpreted relative to the changelog.d directory, or will be found in a few files installed with scriv. - Configuration values can interpolate the currently configured format (rst or md) with "${config:format}". - The default value for new templates is now "file: new_entry.${config:format}.j2". - Configuration values can be read from string literals in Python code with a "literal:" prefix. - "version" is now a configuration setting. This will be most useful when used with the "literal:" prefix. - By default, the title of collected changelog entries includes the version if it's defined. - The collect command now accepts a ``--version`` option to set the version name used in the changelog entry title. Changed ....... - RST now uses minuses instead of equals. - The `create` command now includes the time as well as the date in the entry file name. - The --delete option to collect is now called --keep, and defaults to False. By default, the collected entry files are removed. - Created file names now include the seconds from the current time. - "scriv create" will refuse to overwrite an existing entry file. - Made terminology more uniform: files in changelog.d are "fragments." When collected together, they make one changelog "entry." - The title text for the collected changelog entry is now created from the "entry_title_template" configuration setting. It's a Jinja2 template. - Combined the rst_header_char and rst_section_char settings into one: rst_header_chars, which much be exactly two characters. - Parsing RST fragments is more flexible: the sections can use any valid RST header characters for the underline. Previously, it had to match the configured RST header character. Fixed ..... - Fragments with no category header were being dropped if categories were in use. This is now fixed. Uncategorized fragments get sorted before any categorized fragments. 0.1.0 — 2019-12-30 ------------------ * Doesn't really do anything yet. scriv-1.4.0/LICENSE.txt000066400000000000000000000237011451174641100144740ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS scriv-1.4.0/MANIFEST.in000066400000000000000000000005131451174641100144030ustar00rootroot00000000000000include .editorconfig include CHANGELOG.rst include LICENSE.txt include Makefile include pylintrc include README.rst include tox.ini recursive-include changelog.d * recursive-include docs Makefile *.py *.rst recursive-include docs/_static * recursive-include requirements *.in *.txt recursive-include tests *.py prune doc/_build scriv-1.4.0/Makefile000066400000000000000000000115231451174641100143100ustar00rootroot00000000000000# Makefile for scriv # # To release: # - increment the version in src/scriv/__init__.py # - scriv collect # - commit changes # - make check_release # - make release .PHONY: clean sterile coverage docs help \ quality requirements test test-all upgrade validate .DEFAULT_GOAL := help # For opening files in a browser. Use like: $(BROWSER)relative/path/to/file.html BROWSER := python -m webbrowser file://$(CURDIR)/ help: ## display this help message @echo "Please use \`make ' where is one of" @awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort clean: ## remove generated byte code, coverage reports, and build artifacts find . -name '__pycache__' -exec rm -rf {} + find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + coverage erase rm -fr coverage.json rm -fr build/ rm -fr dist/ rm -fr *.egg-info rm -fr htmlcov/ rm -fr .*_cache/ cd docs; make clean sterile: clean ## remove absolutely all built artifacts rm -fr .tox coverage: clean ## generate and view HTML coverage report tox -e py37,py312,coverage $(BROWSER)htmlcov/index.html docs: botedits ## generate Sphinx HTML documentation, including API docs tox -e docs $(BROWSER)docs/_build/html/index.html PIP_COMPILE = pip-compile --upgrade --resolver=backtracking upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in pip install -qr requirements/pip-tools.txt # Make sure to compile files after any other files they include! $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in $(PIP_COMPILE) -o requirements/base.txt requirements/base.in $(PIP_COMPILE) -o requirements/test.txt requirements/test.in $(PIP_COMPILE) -o requirements/doc.txt requirements/doc.in $(PIP_COMPILE) -o requirements/quality.txt requirements/quality.in $(PIP_COMPILE) -o requirements/tox.txt requirements/tox.in $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in # Splice requirements/base.in into setup.cfg sed -n -e '1,/begin_install_requires/p' < setup.cfg > setup.tmp sed -n -e '/^[a-zA-Z]/s/^/ /p' < requirements/base.in >> setup.tmp sed -n -e '/end_install_requires/,$$p' < setup.cfg >> setup.tmp mv setup.tmp setup.cfg diff_upgrade: ## summarize the last `make upgrade` @# The sort flags sort by the package name first, then by the -/+, and @# sort by version numbers, so we get a summary with lines like this: @# -bashlex==0.16 @# +bashlex==0.17 @# -build==0.9.0 @# +build==0.10.0 @git diff -U0 | grep -v '^@' | grep == | sort -k1.2,1.99 -k1.1,1.1r -u -V botedits: ## make source edits by tools python -m black --line-length=80 src/scriv tests docs setup.py python -m cogapp -crP docs/*.rst quality: ## check coding style with pycodestyle and pylint tox -e quality requirements: ## install development environment requirements pip install -qr requirements/pip-tools.txt pip-sync requirements/dev.txt test: ## run tests in the current virtualenv tox -e py38 test-all: ## run tests on every supported Python combination tox validate: clean botedits quality test ## run tests and quality checks .PHONY: dist pypi testpypi tag gh_release dist: ## Build the distributions python -m build --sdist --wheel pypi: ## Upload the built distributions to PyPI. python -m twine upload --verbose dist/* testpypi: ## Upload the distrubutions to PyPI's testing server. python -m twine upload --verbose --repository testpypi dist/* tag: ## Make a git tag with the version number git tag -a -m "Version $$(python setup.py --version)" $$(python setup.py --version) git push --all gh_release: ## Make a GitHub release python -m scriv github-release --all comment_text: @echo "Use this to comment on issues and pull requests:" @export V=$$(python setup.py --version); \ echo "This is now released as part of [scriv $$V](https://pypi.org/project/scriv/$$V)." .PHONY: release check_release _check_manifest _check_version _check_scriv release: clean check_release dist pypi tag gh_release comment_text ## do all the steps for a release check_release: _check_manifest _check_tree _check_version _check_scriv ## check that we are ready for a release @echo "Release checks passed" _check_manifest: python -m check_manifest _check_tree: @if [[ -n $$(git status --porcelain) ]]; then \ echo 'There are modified files! Did you forget to check them in?'; \ exit 1; \ fi _check_version: @if [[ $$(git tags | grep -q -w $$(python setup.py --version) && echo "x") == "x" ]]; then \ echo 'A git tag for this version exists! Did you forget to bump the version in src/scriv/__init__.py?'; \ exit 1; \ fi _check_scriv: @if [[ $$(find -E changelog.d -regex '.*\.(md|rst)$$') ]]; then \ echo 'There are scriv fragments! Did you forget `scriv collect`?'; \ exit 1; \ fi scriv-1.4.0/README.rst000066400000000000000000000067571451174641100143540ustar00rootroot00000000000000##### Scriv ##### Scriv changelog management tool .. begin-badges | |pypi-badge| |ci-badge| |coverage-badge| |doc-badge| | |pyversions-badge| |license-badge| |sponsor-badge| |mastodon-nedbat| .. end Overview ======== Scriv is a command-line tool for helping developers maintain useful changelogs. It manages a directory of changelog fragments. It aggregates them into entries in a CHANGELOG file. Getting Started =============== Scriv writes changelog fragments into a directory called "changelog.d". Start by creating this directory. (By the way, like many aspects of scriv's operation, you can choose a different name for this directory.) To make a new changelog fragment, use the ``scriv create`` command. It will make a new file with a filename using the current date and time, your GitHub or Git user name, and your branch name. Changelog fragments should be committed along with all the other changes on your branch. When it is time to release your project, the ``scriv collect`` command aggregates all the fragments into a new entry in your changelog file. You can also choose to publish your changelog entries as GitHub releases with the ``scriv github-release`` command. It parses the changelog file and creates or updates GitHub releases to match. It can be used even with changelog files that were not created by scriv. Documentation ============= Full documentation is at https://scriv.readthedocs.org. License ======= The code in this repository is licensed under the Apache Software License 2.0 unless otherwise noted. Please see ``LICENSE.txt`` for details. How To Contribute ================= Contributions are very welcome. Thanks to all the contributors so far: .. begin-contributors | Ned Batchelder | Abhilash Raj | Agustín Piqueres | Flo Kuepper | James Gerity | Javier Sagredo | Kurt McKee | Matias Guijarro | Rodrigo Girão Serrão | Ronny Pfannschmidt .. end .. begin-badge-links .. |pypi-badge| image:: https://img.shields.io/pypi/v/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: PyPI .. |ci-badge| image:: https://github.com/nedbat/scriv/workflows/Test%20Suite/badge.svg :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Build status .. |coverage-badge| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nedbat/5a304c1c779d4bcc57be95f847e9327f/raw/covbadge.json :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Coverage .. |doc-badge| image:: https://readthedocs.org/projects/scriv/badge/?version=latest :target: http://scriv.readthedocs.io/en/latest/ :alt: Documentation .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: Supported Python versions .. |license-badge| image:: https://img.shields.io/github/license/nedbat/scriv.svg :target: https://github.com/nedbat/scriv/blob/master/LICENSE.txt :alt: License .. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=Mastodon :target: https://hachyderm.io/@nedbat :alt: nedbat on Mastodon .. |sponsor-badge| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub :target: https://github.com/sponsors/nedbat :alt: Sponsor me on GitHub .. end scriv-1.4.0/changelog.d/000077500000000000000000000000001451174641100150175ustar00rootroot00000000000000scriv-1.4.0/changelog.d/README.txt000066400000000000000000000001011451174641100165050ustar00rootroot00000000000000This directory will hold the changelog entries managed by scriv. scriv-1.4.0/changelog.d/ghrel_template.md.j2000066400000000000000000000002661451174641100206530ustar00rootroot00000000000000:arrow_right:  PyPI page: [scriv {{version}}](https://pypi.org/project/scriv/{{version}}). :arrow_right:  To install: `python3 -m pip install scriv=={{version}}` {{body}} scriv-1.4.0/docs/000077500000000000000000000000001451174641100135765ustar00rootroot00000000000000scriv-1.4.0/docs/Makefile000066400000000000000000000037501451174641100152430ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/ .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." scriv-1.4.0/docs/_static/000077500000000000000000000000001451174641100152245ustar00rootroot00000000000000scriv-1.4.0/docs/_static/theme_overrides.css000066400000000000000000000005161451174641100211240ustar00rootroot00000000000000/* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } scriv-1.4.0/docs/changelog.rst000066400000000000000000000000361451174641100162560ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst scriv-1.4.0/docs/commands.rst000066400000000000000000000177371451174641100161500ustar00rootroot00000000000000######## Commands ######## .. [[[cog # Force help text to be wrapped narrow enough to not trigger doc8 warnings. import os os.environ["COLUMNS"] = "78" import contextlib import io import textwrap from scriv.cli import cli def show_help(cmd): with contextlib.redirect_stdout(io.StringIO()) as help_out: with contextlib.suppress(SystemExit): cli([cmd, "--help"]) help_text = help_out.getvalue() help_text = help_text.replace("python -m cogapp", "scriv") print("\n.. code::\n") print(f" $ scriv {cmd} --help") print(textwrap.indent(help_text, " ").rstrip()) .. ]]] .. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) .. _cmd_create: scriv create ============ .. [[[cog show_help("create") ]]] .. code:: $ scriv create --help Usage: scriv create [OPTIONS] Create a new changelog fragment. Options: --add / --no-add 'git add' the created file. --edit / --no-edit Open the created file in your text editor. -v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG --help Show this message and exit. .. [[[end]]] (checksum: 45edec1fd1ebc343358cbf774ba5a49c) The create command creates new :ref:`fragments `. File creation ------------- Fragments are created in the changelog.d directory. The name of the directory can be configured with the :ref:`config_fragment_directory` setting. The file name starts with the current date and time, so that entries can later be collected in chronological order. To help make the files understandable, the file name also includes the creator's git name, and the branch name you are working on. "Main" branch names aren't included, to cut down on uninteresting noise. The branch names considered uninteresting are settable with the :ref:`config_main_branches` setting. The initial contents of the fragment file are populated from the :ref:`config_new_fragment_template` template. The format is either reStructuredText or Markdown, selectable with the :ref:`config_format` setting. The default new fragment templates create empty sections for each :ref:`category `. Uncomment the one you want to use, and create a bullet for the changes you are describing. If you need a different template for new fragments, you can create a `Jinja`_ template and name it in the :ref:`config_new_fragment_template` setting. Editing ------- If ``--edit`` is provided, or if ``scriv.create.edit`` is set to true in your :ref:`git settings `, scriv will launch an editor for you to edit the new fragment. Scriv uses the same editor that git launches for commit messages. The format of the fragment should be sections for the categories, with bullets for each change. The file is re-parsed when it is collected, so the specifics of things like header underlines don't have to match the changelog file, that will be adjusted later. Once you save and exit the editor, scriv will continue working on the file. If the file is empty because you removed all of the non-comment content, scriv will stop. Adding ------ If ``--add`` is provided, or if ``scriv.create.add`` is set to true in your :ref:`git settings `, scriv will "git add" the new file so that it is ready to commit. .. _cmd_collect: scriv collect ============= .. [[[cog show_help("collect") ]]] .. code:: $ scriv collect --help Usage: scriv collect [OPTIONS] Collect and combine fragments into the changelog. Options: --add / --no-add 'git add' the updated changelog file and removed fragments. --edit / --no-edit Open the changelog file in your text editor. --title TEXT The title text to use for this entry. --keep Keep the fragment files that are collected. --version TEXT The version name to use for this entry. -v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG --help Show this message and exit. .. [[[end]]] (checksum: e93ca778396310ce406f1cc439cefdd4) The collect command aggregates all the current fragments into the changelog file. Entry Creation -------------- All of the .rst or .md files in the fragment directory are read, parsed, and re-assembled into a changelog entry. The entry's title is determined by the :ref:`config_entry_title_template` setting. The default uses the version string (if one is specified in the :ref:`config_version` setting) and the current date. Instead of using the title template, you can provide an exact title to use for the new entry with the ``--title`` option. The output file is specified by the :ref:`config_output_file` setting. Scriv looks in the file for a special marker (usually in a comment) to determine where to insert the new entry. The marker is "scriv-insert-here", but can be changed with the :ref:`config_insert_marker` setting. Using a marker like this, you can have your changelog be just part of a larger README file. If there is no marker in the file, the new entry is inserted at the top of the file. Fragment Deletion ----------------- The fragment files that are read will be deleted, because they are no longer needed. If you would prefer to keep the fragment files, use the ``--keep`` option. Editing ------- If ``--edit`` is provided, or if ``scriv.collect.edit`` is set to true in your :ref:`git settings `, scriv will launch an editor for you to edit the changelog file. Mostly you shouldn't need to do this, but you might want to make some tweaks. Scriv uses the same editor that git launches for commit messages. Adding ------ If ``--add`` is provided, or if ``scriv.collect.add`` is set to true in your :ref:`git settings `, scriv will "git add" the updates to the changelog file, and the fragment file deletions, so that they are ready to commit. .. _cmd_github_release: scriv github-release ==================== .. [[[cog show_help("github-release") ]]] .. code:: $ scriv github-release --help Usage: scriv github-release [OPTIONS] Create GitHub releases from the changelog. Only the most recent changelog entry is used, unless --all is provided. Options: --all Use all of the changelog entries. --dry-run Don't post to GitHub, just show what would be done. --repo TEXT The GitHub repo (owner/reponame) to create the release in. -v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG --help Show this message and exit. .. [[[end]]] (checksum: eaf0f9e06575bf06499354b22928696b) The ``github-release`` command reads the changelog file, parses it into entries, and then creates or updates GitHub releases to match. Only the most recent changelog entry is used, unless ``--all`` is provided. An entry must have a version number in the title, and that version number must correspond to a git tag. For example, this changelog entry with the title ``v1.2.3 -- 2022-04-06`` will be processed and the version number will be "v1.2.3". If there's a "v1.2.3" git tag, then the entry is a valid release. If there's no detectable version number in the header, or there isn't a git tag with the same number, then the entry can't be created as a GitHub release. This command is independent of the other commands. It can be used with a hand-edited changelog file that wasn't created with scriv. For writing to GitHub, you need a GitHub personal access token, either stored in your .netrc file, or in the GITHUB_TOKEN environment variable. The GitHub repo will be determined by examining the git remotes. If there is just one GitHub repo in the remotes, it will be used to create the release. You can explicitly specify a repo in ``owner/reponame`` form with the ``--repo=`` option if needed. If your changelog file is in reStructuredText format, you will need `pandoc`_ 2.11.2 or later installed for the command to work. .. _pandoc: https://pandoc.org/ .. include:: include/links.rst scriv-1.4.0/docs/concepts.rst000066400000000000000000000023551451174641100161530ustar00rootroot00000000000000######## Concepts ######## .. _fragments: Fragments ========= Fragments are files describing your latest work, created by the ":ref:`cmd_create`" command. The files are created in the changelog.d directory (settable with :ref:`config_fragment_directory`). Typically, they are committed with the code change itself, then later aggregated into the changelog file with ":ref:`cmd_collect`". .. _categories: Categories ========== Changelog entries can be categorized, for example as additions, fixes, removals, and breaking changes. The list of categories is settable with the :ref:`config_categories` setting. If you are using categories in your project, new fragments will be pre-populated with all the categories, commented out. While editing the fragment, you provide your change information in the appropriate category. When the fragments are collected, they are grouped by category into a single changelog entry. You can choose not to use categories by setting the :ref:`config_categories` setting to empty. .. _entries: Entries ======= Fragments are collected into changelog entries with the ":ref:`cmd_collect`" command. The fragments are combined in each category, in chronological order. The entry is given a header with version and date. scriv-1.4.0/docs/conf.py000066400000000000000000000314241451174641100151010ustar00rootroot00000000000000# pylint: disable=invalid-name, redefined-builtin """ Scriv documentation build configuration file. This file is execfile()d with the current directory set to its containing dir. Note that not all possible configuration values are present in this autogenerated file. All configuration values have a default; values that are commented out serve to show the default. """ import os import re import sys # import sphinx_rtd_theme def get_version(*file_paths): """ Extract the version string from a file. """ filename = os.path.join(os.path.dirname(__file__), *file_paths) with open(filename, encoding="utf-8") as version_file: version_text = version_file.read() version_match = re.search( r"^__version__ = ['\"]([^'\"]*)['\"]", version_text, re.M ) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(REPO_ROOT) VERSION = get_version("../src/scriv", "__init__.py") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.ifconfig", "sphinx.ext.napoleon", "sphinx_rtd_theme", ] # A list of warning types to suppress arbitrary warning messages. suppress_warnings = [ "image.nonlocal_uri", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Scriv" copyright = "2019\N{EN DASH}2022, Ned Batchelder" author = "Ned Batchelder" project_title = "scriv" documentation_title = f"{project_title}" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = VERSION # The full version, including alpha/beta/rc tags. release = VERSION # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [ "_build", "include/*", "Thumbs.db", ".DS_Store", ] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = 'scriv v0.1.0' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = f"{project}doc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_target = f"{project}.tex" latex_documents = [ (master_doc, latex_target, documentation_title, author, "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, project_title, documentation_title, [author], 1)] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, project_title, documentation_title, author, project_title, "Scriv changelog management tool", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The basename for the epub file. It defaults to the project name. # epub_basename = project # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to save # visual space. # # epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. # # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # # epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. # # epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. # # epub_tocdepth = 3 # Allow duplicate toc entries. # # epub_tocdup = True # Choose between 'default' and 'includehidden'. # # epub_tocscope = 'default' # Fix unsupported image types using the Pillow. # # epub_fix_images = False # Scale large images. # # epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. # # epub_show_urls = 'inline' # If false, no index is generated. # # epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3", None), } scriv-1.4.0/docs/configuration.rst000066400000000000000000000241511451174641100172020ustar00rootroot00000000000000############# Configuration ############# .. highlight:: ini Scriv tries hard to be adaptable to your project's needs. Many aspects of its behavior can be customized with a settings file. Files Read ========== Scriv will read settings from any of these files: - setup.cfg - tox.ini - pyproject.toml - scriv.ini in the fragment directory ("changelog.d/" by default) In .ini or .cfg files, scriv will read settings from a section named either ``[scriv]`` or ``[tool.scriv]``. A .toml file will only be read if the tomli or tomllib modules is available. You can install scriv with the ``[toml]`` extra to install tomli, or tomllib is available with Python 3.11 or greater. In a .toml file, settings will only be read from the ``[tool.scriv]`` section. All of the possible files will be read, and settings will cascade. So for example, setup.cfg can set the fragment directory to "scriv.d", then "scriv.d/scriv.ini" will be read. The settings examples here show .ini syntax. If you are using a pyproject.toml file for settings, you will need to adjust for TOML syntax. This .ini example:: [scriv] version = literal: pyproject.toml: project.version would become: .. code-block:: toml [tool.scriv] version = "literal: pyproject.toml: project.version" Settings Syntax =============== Settings use the usual syntax, but with some extra features: - A prefix of ``file:`` reads the setting from a file. - A prefix of ``literal:`` reads a literal data from a source file. - A prefix of ``command:`` runs the command and uses the output as the setting. - Value substitutions can make a setting depend on another setting. These are each explained below: File Prefix ----------- A ``file:`` prefix means the setting is a file name or path, and the actual setting value will be read from that file. The file name will be searched for in three places: the fragment directory (changelog.d by default), the current directory, or one of a few built-in templates. If the first path component is ``.`` or ``..``, then only the current directory is considered. Scriv provides two built-in templates: .. [[[cog import textwrap def include_file(fname): """Include a source file into the docs as a code block.""" print(".. code-block:: jinja\n") with open(fname) as f: print(textwrap.indent(f.read(), prefix=" ")) .. ]]] .. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) - ``new_fragment.md.j2``: The default Jinja template for new Markdown fragments: .. [[[cog include_file("src/scriv/templates/new_fragment.md.j2") ]]] .. code-block:: jinja {% for cat in config.categories -%} {% endfor -%} .. [[[end]]] (checksum: 522af8fd44433254fa64c58f89733d4d) - ``new_fragment.rst.j2``: The default Jinja template for new reStructuredText fragments: .. [[[cog include_file("src/scriv/templates/new_fragment.rst.j2") ]]] .. code-block:: jinja .. A new scriv changelog fragment. {% if config.categories -%} .. .. Uncomment the header that is right (remove the leading dots). .. {% for cat in config.categories -%} .. {{ cat }} .. {{ config.rst_header_chars[1] * (cat|length) }} .. .. - A bullet item for the {{ cat }} category. .. {% endfor -%} {% else %} - A bullet item for this fragment. EDIT ME! {% endif -%} .. [[[end]]] (checksum: bdc8c8a24aa1aed2a40d07d08e8a939c) Literal Prefix -------------- A ``literal:`` prefix means the setting value will be a literal string read from a source file. The setting provides a file name and value name separated by colons:: [scriv] version = literal: myproj/__init__.py: __version__ In this case, the file ``myproj/__init__.py`` will be read, and the ``__version__`` value will be found and used as the version setting. Currently Python, .cfg, TOML, YAML and Cabal files are supported for literals, but other syntaxes can be supported in the future. When reading a literal from a TOML file, the value is specified using periods to separate the sections and key names:: [scriv] version = literal: pyproject.toml: project.version For data from a YAML file, use periods in the value name to access dictionary keys:: [scriv] version = literal: galaxy.yaml: myproduct.versionString When using a Cabal file, the version of the package can be accessed using:: [scriv] version = literal: my-package.cabal: version Commands -------- A ``command:`` prefix indicates that the setting is a shell command to run. The output will be used as the setting:: [scriv] version = command: my_version_tool --next Value Substitution ------------------ The chosen fragment format can be used in settings by referencing ``${config:format}`` in the setting. For example, the default template for new fragments depends on the format because the default setting is:: new_fragment_template = file: new_fragment.${config:format}.j2 Settings ======== These are the specifics about all of the settings read from the configuration file. .. [[[cog import attr import textwrap from scriv.config import _Options fields = sorted(attr.fields(_Options), key=lambda f: f.name) for field in fields: name = field.name print(f"\n\n.. _config_{name}:\n") print(name) print("-" * len(name)) print() text = field.metadata.get("doc", "NO DOC!\n") text = textwrap.dedent(text) print(text) default = field.metadata.get("doc_default") if default is None: default = field.default if isinstance(default, list): default = ", ".join(default) default = f"``{default}``" print("\n".join(textwrap.wrap(f"Default: {default}"))) print() .. ]]] .. _config_categories: categories ---------- Categories to use as headings for changelog items. See :ref:`categories`. Default: ``Removed, Added, Changed, Deprecated, Fixed, Security`` .. _config_end_marker: end_marker ---------- A marker string indicating where in the changelog file the changelog ends. Default: ``scriv-end-here`` .. _config_entry_title_template: entry_title_template -------------------- The `Jinja`_ template to use for the entry heading text for changelog entries created by ":ref:`cmd_collect`". Default: ``{% if version %}{{ version }} — {% endif %}{{ date.strftime('%Y-%m-%d') }}`` .. _config_format: format ------ The format to use for fragments and for the output changelog file. Can be either "rst" or "md". Default: ``rst`` .. _config_fragment_directory: fragment_directory ------------------ The directory for fragments. This directory must exist, it will not be created. Default: ``changelog.d`` .. _config_ghrel_template: ghrel_template -------------- The template to use for GitHub releases created by the ``scriv github-release`` command. The extracted Markdown text is available as ``{{body}}``. You must include this to use the text from the changelog file. The version is available as ``{{version}}``. The data for the release is available in a ``{{release}}`` object, including ``{{release.prerelease}}``. It's a boolean, true if this is a pre-release version. The scriv configuration is available in a ``{{config}}`` object. Default: ``{{body}}`` .. _config_insert_marker: insert_marker ------------- A marker string indicating where in the changelog file new entries should be inserted. Default: ``scriv-insert-here`` .. _config_main_branches: main_branches ------------- The branch names considered uninteresting to use in new fragment file names. Default: ``master, main, develop`` .. _config_md_header_level: md_header_level --------------- A number: for Markdown changelog files, this is the heading level to use for the entry heading. Default: ``1`` .. _config_new_fragment_template: new_fragment_template --------------------- The `Jinja`_ template to use for new fragments. Default: ``file: new_fragment.${config:format}.j2`` .. _config_output_file: output_file ----------- The changelog file updated by ":ref:`cmd_collect`". Default: ``CHANGELOG.${config:format}`` .. _config_rst_header_chars: rst_header_chars ---------------- Two characters: for reStructuredText changelog files, these are the two underline characters to use. The first is for the heading for each changelog entry, the second is for the category sections within the entry. Default: ``=-`` .. _config_skip_fragments: skip_fragments -------------- A glob pattern for files in the fragment directory that should not be collected. Default: ``README.*`` .. _config_version: version ------- The string to use as the version number in the next header created by ``scriv collect``. Often, this will be a ``literal:`` directive, to get the version from a string in a source file. Default: (empty) .. [[[end]]] (checksum: 675df32fb207262bd0c69a94a99c2fb7) .. _git_settings: Per-User Git Settings ===================== Some aspects of scriv's behavior are configurable for each user rather than for the project as a whole. These settings are read from git. Editing and Adding ------------------ These settings determine whether the ":ref:`cmd_create`" and ":ref:`cmd_collect`" commands will launch an editor, and "git add" the result: - ``scriv.create.edit`` - ``scriv.create.add`` - ``scriv.collect.edit`` - ``scriv.collect.add`` All of these are either "true" or "false", and default to false. You can create these settings with `git config`_ commands, either in the current repo:: $ git config scriv.create.edit true or globally for all of your repos:: $ git config --global scriv.create.edit true User Nickname ------------- Scriv includes your git or GitHub username in the file names of changelog fragments you create. If you don't like the name it finds for you, you can set a name as the ``scriv.user_nick`` git setting. .. _git config: https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration .. include:: include/links.rst scriv-1.4.0/docs/include/000077500000000000000000000000001451174641100152215ustar00rootroot00000000000000scriv-1.4.0/docs/include/links.rst000066400000000000000000000001301451174641100170650ustar00rootroot00000000000000.. Links to be used elsewhere in the docs .. _Jinja: https://jinja.palletsprojects.com scriv-1.4.0/docs/index.rst000066400000000000000000000105521451174641100154420ustar00rootroot00000000000000##### Scriv ##### .. [[[cog import textwrap def include_readme_section(sectname): """Pull a chunk from README.rst""" with open("README.rst") as freadme: for line in freadme: if f".. begin-{sectname}" in line: break for line in freadme: if ".. end" in line: break print(line.rstrip()) .. ]]] .. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) Scriv changelog management tool .. [[[cog include_readme_section("badges") ]]] | |pypi-badge| |ci-badge| |coverage-badge| |doc-badge| | |pyversions-badge| |license-badge| |sponsor-badge| |mastodon-nedbat| .. [[[end]]] (checksum: 8f16614e7d2eb8fa4e13bacbec12cb24) Overview ======== Scriv is a command-line tool for helping developers maintain useful changelogs. It manages a directory of changelog fragments. It aggregates them into entries in a CHANGELOG file. Currently scriv implements a simple workflow. The goal is to adapt to more styles of changelog management in the future. Getting Started =============== Scriv writes changelog fragments into a directory called "changelog.d". Start by creating this directory. (By the way, like many aspects of scriv's operation, you can choose a different name for this directory.) To make a new changelog fragment, use the ":ref:`cmd_create`" command. It will make a new file with a filename using the current date and time, your GitHub or Git user name, and your branch name. Changelog fragments should be committed along with all the other changes on your branch. When it is time to release your project, the ":ref:`cmd_collect`" command aggregates all the fragments into a new entry in your changelog file. You can also choose to publish your changelog entries as GitHub releases with the ":ref:`cmd_github_release`" command. It parses the changelog file and creates or updates GitHub releases to match. It can be used even with changelog files that were not created by scriv. .. toctree:: :maxdepth: 1 philosophy concepts commands configuration changelog .. scenarios .. lib, every commit published .. app, no version numbers .. lib, occasional publish How To Contribute ================= `Contributions on GitHub `_ are very welcome. Thanks to all the contributors so far: .. [[[cog include_readme_section("contributors") ]]] | Ned Batchelder | Abhilash Raj | Agustín Piqueres | Flo Kuepper | James Gerity | Javier Sagredo | Kurt McKee | Matias Guijarro | Rodrigo Girão Serrão | Ronny Pfannschmidt .. [[[end]]] (checksum: a0df9318cc05969b40b61c5540f9a1b5) .. _repo: https://github.com/nedbat/scriv .. [[[cog include_readme_section("badge-links") ]]] .. |pypi-badge| image:: https://img.shields.io/pypi/v/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: PyPI .. |ci-badge| image:: https://github.com/nedbat/scriv/workflows/Test%20Suite/badge.svg :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Build status .. |coverage-badge| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nedbat/5a304c1c779d4bcc57be95f847e9327f/raw/covbadge.json :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Coverage .. |doc-badge| image:: https://readthedocs.org/projects/scriv/badge/?version=latest :target: http://scriv.readthedocs.io/en/latest/ :alt: Documentation .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: Supported Python versions .. |license-badge| image:: https://img.shields.io/github/license/nedbat/scriv.svg :target: https://github.com/nedbat/scriv/blob/master/LICENSE.txt :alt: License .. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=Mastodon :target: https://hachyderm.io/@nedbat :alt: nedbat on Mastodon .. |sponsor-badge| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub :target: https://github.com/sponsors/nedbat :alt: Sponsor me on GitHub .. [[[end]]] (checksum: 352ff7e93ca0b80b402cbc07179c225f) scriv-1.4.0/docs/philosophy.rst000066400000000000000000000051141451174641100165270ustar00rootroot00000000000000########## Philosophy ########## .. _philosophy: Scriv's design is guided by a few principles: - Changelogs should be captured in a file in the repository. Scriv writes a CHANGELOG file. - Writing about changes to code should happen close in time to the changes themselves. Scriv encourages writing fragment files to be committed when you commit your code changes. - How you describe a change depends on who you are describing it for. You may need multiple descriptions of the same change. Scriv encourages writing changelog entries directly, rather than copying text from commit messages or pull requests. - The changelog file in the repo should be the source of truth. The information can also be published elsewhere, like GitHub releases. - Different projects have different needs; flexibility is a plus. Scriv doesn't assume any particular issue tracker or packaging system, and allows either .rst or .md files. .. _other_tools: Other Tools =========== Scriv is not the first tool to help manage changelogs, there have been many. None fully embodied scriv's philopsophy. Tools most similar to scriv: - `towncrier`_: built for Twisted, with some unusual specifics: fragment type is the file extension, issue numbers in the file name. Only .rst files. - `blurb`_: built for CPython development, specific to their workflow: issue numbers from bugs.python.org, only .rst files. - `setuptools-changelog`_: particular to Python projects (uses a setup.py command), and only supports .rst files. - `gitchangelog`_: collects git commit messages into a changelog file. Tools that only read GitHub issues, or only write GitHub releases: - `Chronicler`_: a web hook that watched for merged pull requests, then appends the pull request message to the most recent draft GitHub release. - `fastrelease`_: reads information from GitHub issues, and writes GitHub releases. - `Release Drafter`_: adds text from merged pull requests to the latest draft GitHub release. Other release note tools: - `reno`_: built for Open Stack. It stores changelogs forever as fragment files, only combining for publication. .. _towncrier: https://github.com/hawkowl/towncrier .. _blurb: https://github.com/python/core-workflow/tree/master/blurb .. _setuptools-changelog: https://pypi.org/project/setuptools-changelog/ .. _gitchangelog: https://pypi.org/project/gitchangelog/ .. _fastrelease: https://fastrelease.fast.ai/ .. _Chronicler: https://github.com/NYTimes/Chronicler .. _Release Drafter: https://probot.github.io/apps/release-drafter/ .. _reno: https://docs.openstack.org/reno/latest/user/usage.html scriv-1.4.0/pylintrc000066400000000000000000000143071451174641100144420ustar00rootroot00000000000000[MASTER] ignore = persistent = yes load-plugins = pylint_pytest [MESSAGES CONTROL] enable = blacklisted-name, line-too-long, syntax-error, init-is-generator, return-in-init, function-redefined, not-in-loop, return-outside-function, yield-outside-function, return-arg-in-generator, nonexistent-operator, duplicate-argument-name, abstract-class-instantiated, bad-reversed-sequence, continue-in-finally, method-hidden, access-member-before-definition, no-method-argument, no-self-argument, invalid-slots-object, assigning-non-slot, invalid-slots, inherit-non-class, inconsistent-mro, duplicate-bases, non-iterator-returned, unexpected-special-method-signature, invalid-length-returned, import-error, used-before-assignment, undefined-variable, undefined-all-variable, invalid-all-object, no-name-in-module, unpacking-non-sequence, bad-except-order, raising-bad-type, misplaced-bare-raise, raising-non-exception, catching-non-exception, bad-super-call, no-member, not-callable, assignment-from-no-return, no-value-for-parameter, too-many-function-args, unexpected-keyword-arg, redundant-keyword-arg, invalid-sequence-index, invalid-slice-index, assignment-from-none, not-context-manager, invalid-unary-operand-type, unsupported-binary-operation, repeated-keyword, not-an-iterable, not-a-mapping, unsupported-membership-test, unsubscriptable-object, logging-unsupported-format, logging-too-many-args, logging-too-few-args, bad-format-character, truncated-format-string, format-needs-mapping, missing-format-string-key, too-many-format-args, too-few-format-args, bad-str-strip-call, unreachable, dangerous-default-value, pointless-statement, pointless-string-statement, expression-not-assigned, duplicate-key, confusing-with-statement, using-constant-test, lost-exception, assert-on-tuple, attribute-defined-outside-init, bad-staticmethod-argument, arguments-differ, signature-differs, abstract-method, super-init-not-called, import-self, misplaced-future, global-variable-undefined, redefined-outer-name, redefined-builtin, undefined-loop-variable, cell-var-from-loop, duplicate-except, binary-op-exception, bad-format-string-key, unused-format-string-key, bad-format-string, missing-format-argument-key, unused-format-string-argument, format-combined-specification, missing-format-attribute, invalid-format-index, anomalous-backslash-in-string, anomalous-unicode-escape-in-string, bad-open-mode, boolean-datetime, fatal, astroid-error, parse-error, method-check-failed, raw-checker-failed, empty-docstring, invalid-characters-in-docstring, missing-docstring, wrong-spelling-in-comment, wrong-spelling-in-docstring, unused-import, unused-variable, unused-argument, exec-used, eval-used, bad-classmethod-argument, bad-mcs-classmethod-argument, bad-mcs-method-argument, consider-iterating-dictionary, consider-using-enumerate, multiple-imports, multiple-statements, singleton-comparison, superfluous-parens, unidiomatic-typecheck, unneeded-not, simplifiable-if-statement, no-classmethod-decorator, no-staticmethod-decorator, unnecessary-pass, unnecessary-lambda, useless-else-on-loop, unnecessary-semicolon, reimported, global-variable-not-assigned, global-at-module-level, bare-except, broad-except, logging-not-lazy, redundant-unittest-assert, protected-access, deprecated-module, deprecated-method, too-many-nested-blocks, too-many-statements, too-many-boolean-expressions, wrong-import-order, wrong-import-position, wildcard-import, missing-final-newline, mixed-line-endings, trailing-newlines, trailing-whitespace, unexpected-line-ending-format, bad-option-value, unrecognized-inline-option, useless-suppression, bad-inline-option, deprecated-pragma, disable = invalid-name, file-ignored, bad-indentation, unused-wildcard-import, global-statement, no-else-return, duplicate-code, fixme, locally-disabled, logging-format-interpolation, logging-fstring-interpolation, suppressed-message, too-few-public-methods, too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-public-methods, too-many-return-statements, ungrouped-imports, [REPORTS] output-format = text reports = no score = no [BASIC] module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ class-rgx = [A-Z_][a-zA-Z0-9]+$ function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$ method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ attr-rgx = [a-z_][a-z0-9_]{2,30}$ argument-rgx = [a-z_][a-z0-9_]{2,30}$ variable-rgx = [a-z_][a-z0-9_]{2,30}$ class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ good-names = f,i,j,k,db,ex,Run,_,__ bad-names = foo,bar,baz,toto,tutu,tata no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ docstring-min-length = 5 [FORMAT] max-line-length = 80 ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ single-line-if-stmt = no max-module-lines = 1000 indent-string = ' ' [MISCELLANEOUS] notes = FIXME,XXX,TODO [SIMILARITIES] min-similarity-lines = 4 ignore-comments = yes ignore-docstrings = yes ignore-imports = no [TYPECHECK] ignore-mixin-members = yes ignored-classes = SQLObject unsafe-load-any-extension = yes generated-members = REQUEST, acl_users, aq_parent, objects, DoesNotExist, can_read, can_write, get_url, size, content, status_code, create, build, fields, tag, org, course, category, name, revision, _meta, [VARIABLES] init-import = no dummy-variables-rgx = _|dummy|unused|.*_unused additional-builtins = [CLASSES] defining-attr-methods = __init__,__new__,setUp valid-classmethod-first-arg = cls valid-metaclass-classmethod-first-arg = mcs [DESIGN] max-args = 5 ignored-argument-names = _.* max-locals = 15 max-returns = 6 max-branches = 12 max-statements = 50 max-parents = 7 max-attributes = 7 min-public-methods = 2 max-public-methods = 20 [IMPORTS] deprecated-modules = regsub,TERMIOS,Bastion,rexec import-graph = ext-import-graph = int-import-graph = [EXCEPTIONS] overgeneral-exceptions = builtins.Exception scriv-1.4.0/requirements/000077500000000000000000000000001451174641100153715ustar00rootroot00000000000000scriv-1.4.0/requirements/base.in000066400000000000000000000001521451174641100166310ustar00rootroot00000000000000# Core requirements for using this application -c constraints.txt attrs click click-log jinja2 requests scriv-1.4.0/requirements/base.txt000066400000000000000000000013241451174641100170440ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # make upgrade # attrs==23.1.0 # via -r requirements/base.in certifi==2023.7.22 # via requests charset-normalizer==3.3.0 # via requests click==8.1.7 # via # -r requirements/base.in # click-log click-log==0.4.0 # via -r requirements/base.in idna==3.4 # via requests importlib-metadata==6.7.0 # via # attrs # click jinja2==3.1.2 # via -r requirements/base.in markupsafe==2.1.3 # via jinja2 requests==2.31.0 # via -r requirements/base.in typing-extensions==4.7.1 # via importlib-metadata urllib3==2.0.6 # via requests zipp==3.15.0 # via importlib-metadata scriv-1.4.0/requirements/constraints.txt000066400000000000000000000014311451174641100205000ustar00rootroot00000000000000# Version constraints for pip-installation. # # This file doesn't install any packages. It specifies version constraints # that will be applied if a package is needed. # # When pinning something here, please provide an explanation of why. Ideally, # link to other information that will help people in the future to remove the # pin when possible. Writing an issue against the offending project and # linking to it here is good. # Not sure why I'm getting cannot-enumerate-pytest-fixtures failures: # https://github.com/reverbc/pylint-pytest/issues/20 pylint-pytest==1.0.3 # docutils is causing problems for sphinx_rtd_theme, but doc8 wants to force it # forward. Hold back doc8 while everyone resolves their issues. # https://github.com/readthedocs/sphinx_rtd_theme/issues/1323 doc8 < 1.0 scriv-1.4.0/requirements/dev.in000066400000000000000000000005201451174641100164740ustar00rootroot00000000000000# Additional requirements for development of this application -c constraints.txt -r pip-tools.txt # pip-tools and its dependencies, for managing requirements files -r quality.txt # Core and quality check dependencies -r tox.txt # tox and related dependencies build # For kitting scriv-1.4.0/requirements/dev.txt000066400000000000000000000217101451174641100167110ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # make upgrade # alabaster==0.7.13 # via # -r requirements/quality.txt # sphinx astroid==2.15.8 # via # -r requirements/quality.txt # pylint attrs==23.1.0 # via -r requirements/quality.txt babel==2.13.0 # via # -r requirements/quality.txt # sphinx black==23.3.0 # via -r requirements/quality.txt bleach==6.0.0 # via # -r requirements/quality.txt # readme-renderer build==1.0.3 # via # -r requirements/dev.in # -r requirements/pip-tools.txt # check-manifest # pip-tools cachetools==5.3.1 # via # -r requirements/tox.txt # tox certifi==2023.7.22 # via # -r requirements/quality.txt # requests chardet==5.2.0 # via # -r requirements/tox.txt # tox charset-normalizer==3.3.0 # via # -r requirements/quality.txt # requests check-manifest==0.49 # via -r requirements/quality.txt click==8.1.7 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # black # click-log # pip-tools click-log==0.4.0 # via -r requirements/quality.txt cogapp==3.3.0 # via -r requirements/quality.txt colorama==0.4.6 # via # -r requirements/tox.txt # tox coverage==7.2.7 # via -r requirements/quality.txt dill==0.3.7 # via # -r requirements/quality.txt # pylint distlib==0.3.7 # via # -r requirements/tox.txt # virtualenv doc8==0.11.2 # via -r requirements/quality.txt docutils==0.18.1 # via # -r requirements/quality.txt # doc8 # readme-renderer # restructuredtext-lint # sphinx # sphinx-rtd-theme exceptiongroup==1.1.3 # via # -r requirements/quality.txt # pytest filelock==3.12.2 # via # -r requirements/tox.txt # tox # virtualenv freezegun==1.2.2 # via -r requirements/quality.txt idna==3.4 # via # -r requirements/quality.txt # requests imagesize==1.4.1 # via # -r requirements/quality.txt # sphinx importlib-metadata==6.7.0 # via # -r requirements/pip-tools.txt # -r requirements/tox.txt # attrs # build # click # keyring # pluggy # pytest # sphinx # stevedore # tox # twine # virtualenv importlib-resources==5.12.0 # via # -r requirements/quality.txt # keyring iniconfig==2.0.0 # via # -r requirements/quality.txt # pytest isort==5.11.5 # via # -r requirements/quality.txt # pylint jaraco-classes==3.2.3 # via # -r requirements/quality.txt # keyring jedi==0.19.1 # via # -r requirements/quality.txt # pudb jinja2==3.1.2 # via # -r requirements/quality.txt # sphinx keyring==24.1.1 # via # -r requirements/quality.txt # twine lazy-object-proxy==1.9.0 # via # -r requirements/quality.txt # astroid markdown-it-py==2.2.0 # via # -r requirements/quality.txt # rich markupsafe==2.1.3 # via # -r requirements/quality.txt # jinja2 mccabe==0.7.0 # via # -r requirements/quality.txt # pylint mdurl==0.1.2 # via # -r requirements/quality.txt # markdown-it-py more-itertools==9.1.0 # via # -r requirements/quality.txt # jaraco-classes mypy==1.4.1 # via -r requirements/quality.txt mypy-extensions==1.0.0 # via # -r requirements/quality.txt # black # mypy packaging==23.2 # via # -r requirements/pip-tools.txt # -r requirements/tox.txt # black # build # pudb # pyproject-api # pytest # sphinx # tox parso==0.8.3 # via # -r requirements/quality.txt # jedi pathspec==0.11.2 # via # -r requirements/quality.txt # black pbr==5.11.1 # via # -r requirements/quality.txt # stevedore pip-tools==6.14.0 # via -r requirements/pip-tools.txt pkginfo==1.9.6 # via # -r requirements/quality.txt # twine platformdirs==3.11.0 # via # -r requirements/quality.txt # -r requirements/tox.txt # black # pylint # tox # virtualenv pluggy==1.2.0 # via # -r requirements/quality.txt # -r requirements/tox.txt # pytest # tox pudb==2022.1.3 # via -r requirements/quality.txt pycodestyle==2.10.0 # via -r requirements/quality.txt pydocstyle==6.1.1 # via -r requirements/quality.txt pygments==2.16.1 # via # -r requirements/quality.txt # doc8 # pudb # readme-renderer # rich # sphinx pylint==2.17.7 # via # -r requirements/quality.txt # pylint-pytest pylint-pytest==1.0.3 # via -r requirements/quality.txt pyproject-api==1.5.3 # via # -r requirements/tox.txt # tox pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # build pytest==7.4.2 # via # -r requirements/quality.txt # pylint-pytest # pytest-mock pytest-mock==3.11.1 # via -r requirements/quality.txt python-dateutil==2.8.2 # via # -r requirements/quality.txt # freezegun pytz==2023.3.post1 # via # -r requirements/quality.txt # babel pyyaml==6.0.1 # via # -r requirements/quality.txt # responses readme-renderer==37.3 # via # -r requirements/quality.txt # twine requests==2.31.0 # via # -r requirements/quality.txt # requests-toolbelt # responses # sphinx # twine requests-toolbelt==1.0.0 # via # -r requirements/quality.txt # twine responses==0.23.3 # via -r requirements/quality.txt restructuredtext-lint==1.4.0 # via # -r requirements/quality.txt # doc8 rfc3986==2.0.0 # via # -r requirements/quality.txt # twine rich==13.6.0 # via # -r requirements/quality.txt # twine six==1.16.0 # via # -r requirements/quality.txt # bleach # python-dateutil snowballstemmer==2.2.0 # via # -r requirements/quality.txt # pydocstyle # sphinx sphinx==5.3.0 # via # -r requirements/quality.txt # sphinx-rtd-theme # sphinxcontrib-jquery sphinx-rtd-theme==1.3.0 # via -r requirements/quality.txt sphinxcontrib-applehelp==1.0.2 # via # -r requirements/quality.txt # sphinx sphinxcontrib-devhelp==1.0.2 # via # -r requirements/quality.txt # sphinx sphinxcontrib-htmlhelp==2.0.0 # via # -r requirements/quality.txt # sphinx sphinxcontrib-jquery==4.1 # via # -r requirements/quality.txt # sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via # -r requirements/quality.txt # sphinx sphinxcontrib-qthelp==1.0.3 # via # -r requirements/quality.txt # sphinx sphinxcontrib-serializinghtml==1.1.5 # via # -r requirements/quality.txt # sphinx stevedore==3.5.2 # via # -r requirements/quality.txt # doc8 tomli==2.0.1 # via # -r requirements/pip-tools.txt # -r requirements/tox.txt # black # build # check-manifest # mypy # pip-tools # pylint # pyproject-api # pyproject-hooks # pytest # tox tomlkit==0.12.1 # via # -r requirements/quality.txt # pylint tox==4.8.0 # via -r requirements/tox.txt twine==4.0.2 # via -r requirements/quality.txt typed-ast==1.5.5 # via # -r requirements/quality.txt # astroid # black # mypy types-freezegun==1.1.10 # via -r requirements/quality.txt types-pyyaml==6.0.12.12 # via # -r requirements/quality.txt # responses types-requests==2.31.0.7 # via -r requirements/quality.txt types-toml==0.10.8.7 # via -r requirements/quality.txt typing-extensions==4.7.1 # via # -r requirements/pip-tools.txt # -r requirements/tox.txt # astroid # black # importlib-metadata # markdown-it-py # mypy # platformdirs # pylint # responses # rich # tox urllib3==2.0.6 # via # -r requirements/quality.txt # requests # responses # twine # types-requests urwid==2.2.2 # via # -r requirements/quality.txt # pudb # urwid-readline urwid-readline==0.13 # via # -r requirements/quality.txt # pudb virtualenv==20.24.5 # via # -r requirements/tox.txt # tox webencodings==0.5.1 # via # -r requirements/quality.txt # bleach wheel==0.41.2 # via # -r requirements/pip-tools.txt # pip-tools wrapt==1.15.0 # via # -r requirements/quality.txt # astroid zipp==3.15.0 # via # -r requirements/pip-tools.txt # -r requirements/tox.txt # importlib-metadata # importlib-resources # The following packages are considered to be unsafe in a requirements file: # pip # setuptools scriv-1.4.0/requirements/doc.in000066400000000000000000000004751451174641100164740ustar00rootroot00000000000000# Requirements for documentation validation -c constraints.txt -r test.txt # Core and testing dependencies for this package cogapp doc8 # reStructuredText style checker Sphinx # Documentation builder sphinx-rtd-theme # To make it look like readthedocs scriv-1.4.0/requirements/doc.txt000066400000000000000000000071271451174641100167060ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # make upgrade # alabaster==0.7.13 # via sphinx attrs==23.1.0 # via -r requirements/test.txt babel==2.13.0 # via sphinx certifi==2023.7.22 # via # -r requirements/test.txt # requests charset-normalizer==3.3.0 # via # -r requirements/test.txt # requests click==8.1.7 # via # -r requirements/test.txt # click-log click-log==0.4.0 # via -r requirements/test.txt cogapp==3.3.0 # via -r requirements/doc.in coverage==7.2.7 # via -r requirements/test.txt doc8==0.11.2 # via -r requirements/doc.in docutils==0.18.1 # via # doc8 # restructuredtext-lint # sphinx # sphinx-rtd-theme exceptiongroup==1.1.3 # via # -r requirements/test.txt # pytest freezegun==1.2.2 # via -r requirements/test.txt idna==3.4 # via # -r requirements/test.txt # requests imagesize==1.4.1 # via sphinx importlib-metadata==6.7.0 # via # -r requirements/test.txt # attrs # click # pluggy # pytest # sphinx # stevedore iniconfig==2.0.0 # via # -r requirements/test.txt # pytest jedi==0.19.1 # via # -r requirements/test.txt # pudb jinja2==3.1.2 # via # -r requirements/test.txt # sphinx markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 packaging==23.2 # via # -r requirements/test.txt # pudb # pytest # sphinx parso==0.8.3 # via # -r requirements/test.txt # jedi pbr==5.11.1 # via stevedore pluggy==1.2.0 # via # -r requirements/test.txt # pytest pudb==2022.1.3 # via -r requirements/test.txt pygments==2.16.1 # via # -r requirements/test.txt # doc8 # pudb # sphinx pytest==7.4.2 # via # -r requirements/test.txt # pytest-mock pytest-mock==3.11.1 # via -r requirements/test.txt python-dateutil==2.8.2 # via # -r requirements/test.txt # freezegun pytz==2023.3.post1 # via babel pyyaml==6.0.1 # via # -r requirements/test.txt # responses requests==2.31.0 # via # -r requirements/test.txt # responses # sphinx responses==0.23.3 # via -r requirements/test.txt restructuredtext-lint==1.4.0 # via doc8 six==1.16.0 # via # -r requirements/test.txt # python-dateutil snowballstemmer==2.2.0 # via sphinx sphinx==5.3.0 # via # -r requirements/doc.in # sphinx-rtd-theme # sphinxcontrib-jquery sphinx-rtd-theme==1.3.0 # via -r requirements/doc.in sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-htmlhelp==2.0.0 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx stevedore==3.5.2 # via doc8 tomli==2.0.1 # via # -r requirements/test.txt # pytest types-pyyaml==6.0.12.12 # via # -r requirements/test.txt # responses typing-extensions==4.7.1 # via # -r requirements/test.txt # importlib-metadata # responses urllib3==2.0.6 # via # -r requirements/test.txt # requests # responses urwid==2.2.2 # via # -r requirements/test.txt # pudb # urwid-readline urwid-readline==0.13 # via # -r requirements/test.txt # pudb zipp==3.15.0 # via # -r requirements/test.txt # importlib-metadata scriv-1.4.0/requirements/pip-tools.in000066400000000000000000000002751451174641100176530ustar00rootroot00000000000000# Just the dependencies to run pip-tools, mainly for the "upgrade" make target -c constraints.txt pip-tools # Contains pip-compile, used to generate pip requirements files scriv-1.4.0/requirements/pip-tools.txt000066400000000000000000000012441451174641100200610ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # make upgrade # build==1.0.3 # via pip-tools click==8.1.7 # via pip-tools importlib-metadata==6.7.0 # via # build # click packaging==23.2 # via build pip-tools==6.14.0 # via -r requirements/pip-tools.in pyproject-hooks==1.0.0 # via build tomli==2.0.1 # via # build # pip-tools # pyproject-hooks typing-extensions==4.7.1 # via importlib-metadata wheel==0.41.2 # via pip-tools zipp==3.15.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip # setuptools scriv-1.4.0/requirements/quality.in000066400000000000000000000013171451174641100174130ustar00rootroot00000000000000# Requirements for code quality checks -c constraints.txt -r test.txt # Core and testing dependencies for this package -r doc.txt # Need doc packages for full linting black # Uncompromising code formatting check-manifest # are we packaging files properly? isort # to standardize order of imports mypy # Static type checking pycodestyle # PEP 8 compliance validation pydocstyle # PEP 257 compliance validation pylint pylint-pytest # Understanding of pytest fixtures. twine # For checking distributions types-freezegun types-requests types-toml types-pyyamlscriv-1.4.0/requirements/quality.txt000066400000000000000000000170761451174641100176350ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # make upgrade # alabaster==0.7.13 # via # -r requirements/doc.txt # sphinx astroid==2.15.8 # via pylint attrs==23.1.0 # via # -r requirements/doc.txt # -r requirements/test.txt babel==2.13.0 # via # -r requirements/doc.txt # sphinx black==23.3.0 # via -r requirements/quality.in bleach==6.0.0 # via readme-renderer build==1.0.3 # via check-manifest certifi==2023.7.22 # via # -r requirements/doc.txt # -r requirements/test.txt # requests charset-normalizer==3.3.0 # via # -r requirements/doc.txt # -r requirements/test.txt # requests check-manifest==0.49 # via -r requirements/quality.in click==8.1.7 # via # -r requirements/doc.txt # -r requirements/test.txt # black # click-log click-log==0.4.0 # via # -r requirements/doc.txt # -r requirements/test.txt cogapp==3.3.0 # via -r requirements/doc.txt coverage==7.2.7 # via # -r requirements/doc.txt # -r requirements/test.txt dill==0.3.7 # via pylint doc8==0.11.2 # via -r requirements/doc.txt docutils==0.18.1 # via # -r requirements/doc.txt # doc8 # readme-renderer # restructuredtext-lint # sphinx # sphinx-rtd-theme exceptiongroup==1.1.3 # via # -r requirements/doc.txt # -r requirements/test.txt # pytest freezegun==1.2.2 # via # -r requirements/doc.txt # -r requirements/test.txt idna==3.4 # via # -r requirements/doc.txt # -r requirements/test.txt # requests imagesize==1.4.1 # via # -r requirements/doc.txt # sphinx importlib-metadata==6.7.0 # via # -r requirements/doc.txt # -r requirements/test.txt # attrs # build # click # keyring # pluggy # pytest # sphinx # stevedore # twine importlib-resources==5.12.0 # via keyring iniconfig==2.0.0 # via # -r requirements/doc.txt # -r requirements/test.txt # pytest isort==5.11.5 # via # -r requirements/quality.in # pylint jaraco-classes==3.2.3 # via keyring jedi==0.19.1 # via # -r requirements/doc.txt # -r requirements/test.txt # pudb jinja2==3.1.2 # via # -r requirements/doc.txt # -r requirements/test.txt # sphinx keyring==24.1.1 # via twine lazy-object-proxy==1.9.0 # via astroid markdown-it-py==2.2.0 # via rich markupsafe==2.1.3 # via # -r requirements/doc.txt # -r requirements/test.txt # jinja2 mccabe==0.7.0 # via pylint mdurl==0.1.2 # via markdown-it-py more-itertools==9.1.0 # via jaraco-classes mypy==1.4.1 # via -r requirements/quality.in mypy-extensions==1.0.0 # via # black # mypy packaging==23.2 # via # -r requirements/doc.txt # -r requirements/test.txt # black # build # pudb # pytest # sphinx parso==0.8.3 # via # -r requirements/doc.txt # -r requirements/test.txt # jedi pathspec==0.11.2 # via black pbr==5.11.1 # via # -r requirements/doc.txt # stevedore pkginfo==1.9.6 # via twine platformdirs==3.11.0 # via # black # pylint pluggy==1.2.0 # via # -r requirements/doc.txt # -r requirements/test.txt # pytest pudb==2022.1.3 # via # -r requirements/doc.txt # -r requirements/test.txt pycodestyle==2.10.0 # via -r requirements/quality.in pydocstyle==6.1.1 # via -r requirements/quality.in pygments==2.16.1 # via # -r requirements/doc.txt # -r requirements/test.txt # doc8 # pudb # readme-renderer # rich # sphinx pylint==2.17.7 # via # -r requirements/quality.in # pylint-pytest pylint-pytest==1.0.3 # via -r requirements/quality.in pyproject-hooks==1.0.0 # via build pytest==7.4.2 # via # -r requirements/doc.txt # -r requirements/test.txt # pylint-pytest # pytest-mock pytest-mock==3.11.1 # via # -r requirements/doc.txt # -r requirements/test.txt python-dateutil==2.8.2 # via # -r requirements/doc.txt # -r requirements/test.txt # freezegun pytz==2023.3.post1 # via # -r requirements/doc.txt # babel pyyaml==6.0.1 # via # -r requirements/doc.txt # -r requirements/test.txt # responses readme-renderer==37.3 # via twine requests==2.31.0 # via # -r requirements/doc.txt # -r requirements/test.txt # requests-toolbelt # responses # sphinx # twine requests-toolbelt==1.0.0 # via twine responses==0.23.3 # via # -r requirements/doc.txt # -r requirements/test.txt restructuredtext-lint==1.4.0 # via # -r requirements/doc.txt # doc8 rfc3986==2.0.0 # via twine rich==13.6.0 # via twine six==1.16.0 # via # -r requirements/doc.txt # -r requirements/test.txt # bleach # python-dateutil snowballstemmer==2.2.0 # via # -r requirements/doc.txt # pydocstyle # sphinx sphinx==5.3.0 # via # -r requirements/doc.txt # sphinx-rtd-theme # sphinxcontrib-jquery sphinx-rtd-theme==1.3.0 # via -r requirements/doc.txt sphinxcontrib-applehelp==1.0.2 # via # -r requirements/doc.txt # sphinx sphinxcontrib-devhelp==1.0.2 # via # -r requirements/doc.txt # sphinx sphinxcontrib-htmlhelp==2.0.0 # via # -r requirements/doc.txt # sphinx sphinxcontrib-jquery==4.1 # via # -r requirements/doc.txt # sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via # -r requirements/doc.txt # sphinx sphinxcontrib-qthelp==1.0.3 # via # -r requirements/doc.txt # sphinx sphinxcontrib-serializinghtml==1.1.5 # via # -r requirements/doc.txt # sphinx stevedore==3.5.2 # via # -r requirements/doc.txt # doc8 tomli==2.0.1 # via # -r requirements/doc.txt # -r requirements/test.txt # black # build # check-manifest # mypy # pylint # pyproject-hooks # pytest tomlkit==0.12.1 # via pylint twine==4.0.2 # via -r requirements/quality.in typed-ast==1.5.5 # via # astroid # black # mypy types-freezegun==1.1.10 # via -r requirements/quality.in types-pyyaml==6.0.12.12 # via # -r requirements/quality.in # -r requirements/test.txt # responses types-requests==2.31.0.7 # via -r requirements/quality.in types-toml==0.10.8.7 # via -r requirements/quality.in typing-extensions==4.7.1 # via # -r requirements/doc.txt # -r requirements/test.txt # astroid # black # importlib-metadata # markdown-it-py # mypy # platformdirs # pylint # responses # rich urllib3==2.0.6 # via # -r requirements/doc.txt # -r requirements/test.txt # requests # responses # twine # types-requests urwid==2.2.2 # via # -r requirements/doc.txt # -r requirements/test.txt # pudb # urwid-readline urwid-readline==0.13 # via # -r requirements/doc.txt # -r requirements/test.txt # pudb webencodings==0.5.1 # via bleach wrapt==1.15.0 # via astroid zipp==3.15.0 # via # -r requirements/doc.txt # -r requirements/test.txt # importlib-metadata # importlib-resources # The following packages are considered to be unsafe in a requirements file: # setuptools scriv-1.4.0/requirements/test.in000066400000000000000000000005631451174641100167040ustar00rootroot00000000000000# Requirements for test runs. -c constraints.txt -r base.txt # Core dependencies for this package coverage # for measuring coverage freezegun # for mocking datetime pudb # for when we need to debug pytest-mock # pytest wrapper around mock responses # mock requests pyyamlscriv-1.4.0/requirements/test.txt000066400000000000000000000037231451174641100171160ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # make upgrade # attrs==23.1.0 # via -r requirements/base.txt certifi==2023.7.22 # via # -r requirements/base.txt # requests charset-normalizer==3.3.0 # via # -r requirements/base.txt # requests click==8.1.7 # via # -r requirements/base.txt # click-log click-log==0.4.0 # via -r requirements/base.txt coverage==7.2.7 # via -r requirements/test.in exceptiongroup==1.1.3 # via pytest freezegun==1.2.2 # via -r requirements/test.in idna==3.4 # via # -r requirements/base.txt # requests importlib-metadata==6.7.0 # via # -r requirements/base.txt # attrs # click # pluggy # pytest iniconfig==2.0.0 # via pytest jedi==0.19.1 # via pudb jinja2==3.1.2 # via -r requirements/base.txt markupsafe==2.1.3 # via # -r requirements/base.txt # jinja2 packaging==23.2 # via # pudb # pytest parso==0.8.3 # via jedi pluggy==1.2.0 # via pytest pudb==2022.1.3 # via -r requirements/test.in pygments==2.16.1 # via pudb pytest==7.4.2 # via pytest-mock pytest-mock==3.11.1 # via -r requirements/test.in python-dateutil==2.8.2 # via freezegun pyyaml==6.0.1 # via # -r requirements/test.in # responses requests==2.31.0 # via # -r requirements/base.txt # responses responses==0.23.3 # via -r requirements/test.in six==1.16.0 # via python-dateutil tomli==2.0.1 # via pytest types-pyyaml==6.0.12.12 # via responses typing-extensions==4.7.1 # via # -r requirements/base.txt # importlib-metadata # responses urllib3==2.0.6 # via # -r requirements/base.txt # requests # responses urwid==2.2.2 # via # pudb # urwid-readline urwid-readline==0.13 # via pudb zipp==3.15.0 # via # -r requirements/base.txt # importlib-metadata scriv-1.4.0/requirements/tox.in000066400000000000000000000001601451174641100165300ustar00rootroot00000000000000# Tox and related requirements. -c constraints.txt tox # Virtualenv management for tests scriv-1.4.0/requirements/tox.txt000066400000000000000000000015041451174641100167440ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # make upgrade # cachetools==5.3.1 # via tox chardet==5.2.0 # via tox colorama==0.4.6 # via tox distlib==0.3.7 # via virtualenv filelock==3.12.2 # via # tox # virtualenv importlib-metadata==6.7.0 # via # pluggy # tox # virtualenv packaging==23.2 # via # pyproject-api # tox platformdirs==3.11.0 # via # tox # virtualenv pluggy==1.2.0 # via tox pyproject-api==1.5.3 # via tox tomli==2.0.1 # via # pyproject-api # tox tox==4.8.0 # via -r requirements/tox.in typing-extensions==4.7.1 # via # importlib-metadata # platformdirs # tox virtualenv==20.24.5 # via tox zipp==3.15.0 # via importlib-metadata scriv-1.4.0/setup.cfg000066400000000000000000000054661451174641100145020ustar00rootroot00000000000000[metadata] name = scriv version = attr: scriv.__version__ description = Scriv changelog management tool long_description = file: README.rst, CHANGELOG.rst long_description_content_type = text/x-rst url = https://github.com/nedbat/scriv author = Ned Batchelder author_email = ned@nedbatchelder.com license = Apache-2.0 zip_safe = False classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Natural Language :: English Programming Language :: Python :: 3 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 project_urls = # For some reason, these appear in reverse order on pypi... Mastodon = https://hachyderm.io/@nedbat Funding = https://github.com/sponsors/nedbat Issues = https://github.com/nedbat/scriv/issues Source = https://github.com/nedbat/scriv Documentation = https://scriv.readthedocs.io [options] packages = scriv package_dir = = src install_requires = # begin_install_requires attrs click click-log jinja2 requests # end_install_requires [options.package_data] scriv = templates/*.* [options.entry_points] console_scripts = scriv = scriv.cli:cli [options.extras_require] toml = tomli; python_version < "3.11" yaml = pyyaml [scriv] ghrel_template = file: ghrel_template.md.j2 rst_header_chars = -. version = literal: src/scriv/__init__.py: __version__ [isort] indent = ' ' line_length = 80 multi_line_output = 3 include_trailing_comma = True [wheel] universal = 1 [tool:pytest] addopts = -rfe norecursedirs = .* docs requirements [coverage:run] branch = True source = scriv tests omit = */__main__.py [coverage:report] precision = 2 exclude_also = def __repr__ [coverage:paths] source = src */site-packages others = . */scriv [mypy] python_version = 3.7 show_column_numbers = true show_error_codes = true ignore_missing_imports = true check_untyped_defs = true warn_return_any = true [doc8] max-line-length = 80 [pycodestyle] exclude = .git,.tox max-line-length = 80 ; E203 = whitespace before ':' ; W503 line break before binary operator ignore = E203,W503 [pydocstyle] ; D105 = Missing docstring in magic method ; D200 = One-line docstring should fit on one line with quotes ; D203 = 1 blank line required before class docstring ; D212 = Multi-line docstring summary should start at the first line ; D406 = Section name should end with a newline (numpy style) ; D407 = Missing dashed underline after section (numpy style) ; D413 = Missing blank line after last section (numpy style) ignore = D105,D200,D203,D212,D406,D407,D413 scriv-1.4.0/setup.py000077500000000000000000000001221451174641100143560ustar00rootroot00000000000000#!/usr/bin/env python """Install scriv.""" import setuptools setuptools.setup() scriv-1.4.0/src/000077500000000000000000000000001451174641100134355ustar00rootroot00000000000000scriv-1.4.0/src/scriv/000077500000000000000000000000001451174641100145635ustar00rootroot00000000000000scriv-1.4.0/src/scriv/__init__.py000066400000000000000000000001001451174641100166630ustar00rootroot00000000000000""" Scriv changelog management tool. """ __version__ = "1.4.0" scriv-1.4.0/src/scriv/__main__.py000066400000000000000000000001161451174641100166530ustar00rootroot00000000000000"""Enable 'python -m scriv'.""" from .cli import cli cli(prog_name="scriv") scriv-1.4.0/src/scriv/changelog.py000066400000000000000000000074471451174641100171000ustar00rootroot00000000000000"""Changelog and Fragment definitions for Scriv.""" import datetime import logging from pathlib import Path import attr import jinja2 from .config import Config from .format import FormatTools, SectionDict, get_format_tools from .util import partition_lines logger = logging.getLogger(__name__) @attr.s class Fragment: """A changelog fragment.""" path = attr.ib(type=Path) format = attr.ib(type=str, default=None) content = attr.ib(type=str, default=None) def __attrs_post_init__( self, ): # noqa: D105 (Missing docstring in magic method) if self.format is None: self.format = self.path.suffix.lstrip(".") def write(self) -> None: """Write the content to the file.""" self.path.write_text(self.content) def read(self) -> None: """Read the content of the fragment.""" self.content = self.path.read_text() @attr.s class Changelog: """A changelog file.""" path = attr.ib(type=Path) config = attr.ib(type=Config) newline = attr.ib(type=str, default="") text_before = attr.ib(type=str, default="") changelog = attr.ib(type=str, default="") text_after = attr.ib(type=str, default="") def read(self) -> None: """Read the changelog if it exists.""" logger.info(f"Reading changelog {self.path}") if self.path.exists(): with self.path.open("r", encoding="utf-8") as f: changelog_text = f.read() if f.newlines: # .newlines may be None, str, or tuple if isinstance(f.newlines, str): self.newline = f.newlines else: self.newline = f.newlines[0] before, marker, after = partition_lines( changelog_text, self.config.insert_marker ) if marker: self.text_before = before + marker rest = after else: self.text_before = "" rest = before self.changelog, marker, after = partition_lines( rest, self.config.end_marker ) self.text_after = marker + after else: logger.warning(f"Changelog {self.path} doesn't exist") def format_tools(self) -> FormatTools: """Get the appropriate FormatTools for this changelog.""" return get_format_tools(self.config.format, self.config) def entry_header(self, version, date=None) -> str: """Format the header for a new entry.""" title_data = { "date": date or datetime.datetime.now(), "version": version, } title_template = jinja2.Template(self.config.entry_title_template) new_title = title_template.render(config=self.config, **title_data) if new_title.strip(): anchor = f"changelog-{version}" if version else None new_header = self.format_tools().format_header( new_title, anchor=anchor ) else: new_header = "" return new_header def entry_text(self, sections: SectionDict) -> str: """Format the text of a new entry.""" return self.format_tools().format_sections(sections) def add_entry(self, header: str, text: str) -> None: """Add a new entry to the top of the changelog.""" self.changelog = header + text + self.changelog def write(self) -> None: """Write the changelog.""" f = self.path.open("w", encoding="utf-8", newline=self.newline or None) with f: f.write(self.text_before) f.write(self.changelog) f.write(self.text_after) def entries(self) -> SectionDict: """Parse the changelog into a SectionDict.""" return self.format_tools().parse_text(self.changelog) scriv-1.4.0/src/scriv/cli.py000066400000000000000000000011151451174641100157020ustar00rootroot00000000000000"""Scriv command-line interface.""" import logging import click import click_log from . import __version__ from .collect import collect from .create import create from .ghrel import github_release # Configure the root logger, so all logging works. click_log.basic_config(logging.getLogger()) @click.group( help=f"""\ Manage changelogs. Version {__version__} """ ) @click.version_option() def cli() -> None: # noqa: D401 """The main entry point for the scriv command.""" cli.add_command(create) cli.add_command(collect) cli.add_command(github_release) scriv-1.4.0/src/scriv/collect.py000066400000000000000000000057021451174641100165660ustar00rootroot00000000000000"""Collecting fragments.""" import logging import sys from typing import Optional import click import click_log from .gitinfo import git_add, git_config_bool, git_edit, git_rm from .scriv import Scriv from .util import Version logger = logging.getLogger(__name__) @click.command() @click.option( "--add/--no-add", default=None, help="'git add' the updated changelog file and removed fragments.", ) @click.option( "--edit/--no-edit", default=None, help="Open the changelog file in your text editor.", ) @click.option( "--title", default=None, help="The title text to use for this entry." ) @click.option( "--keep", is_flag=True, help="Keep the fragment files that are collected." ) @click.option( "--version", default=None, help="The version name to use for this entry." ) @click_log.simple_verbosity_option() def collect( add: Optional[bool], edit: Optional[bool], title: str, keep: bool, version: str, ) -> None: """ Collect and combine fragments into the changelog. """ if title is not None and version is not None: sys.exit("Can't provide both --title and --version.") if add is None: add = git_config_bool("scriv.collect.add") if edit is None: edit = git_config_bool("scriv.collect.edit") scriv = Scriv() logger.info(f"Collecting from {scriv.config.fragment_directory}") frags = scriv.fragments_to_combine() if not frags: logger.info("No changelog fragments to collect") sys.exit(2) changelog = scriv.changelog() changelog.read() if title is None: version = Version(version or scriv.config.version) if version: # Check that we haven't used this version before. for etitle in changelog.entries().keys(): if etitle is None: continue eversion = Version.from_text(etitle) if eversion is None: sys.exit( f"Entry {etitle!r} is not a valid version! " + "If scriv should ignore this heading, add " + "'scriv-end-here' somewhere before it." ) if eversion == version: sys.exit( f"Entry {etitle!r} already uses " + f"version {str(version)!r}." ) new_header = changelog.entry_header(version=version) else: new_header = changelog.format_tools().format_header(title) new_text = changelog.entry_text(scriv.combine_fragments(frags)) changelog.add_entry(new_header, new_text) changelog.write() if edit: git_edit(changelog.path) if add: git_add(changelog.path) if not keep: for frag in frags: logger.info(f"Deleting fragment file {str(frag.path)!r}") if add: git_rm(frag.path) else: frag.path.unlink() scriv-1.4.0/src/scriv/config.py000066400000000000000000000335331451174641100164110ustar00rootroot00000000000000"""Scriv configuration.""" import configparser import contextlib import logging import pkgutil import re from pathlib import Path from typing import Any, List import attr from .exceptions import ScrivException from .literals import find_literal from .optional import tomllib from .shell import run_shell_command logger = logging.getLogger(__name__) @attr.s class _Options: """ All the settable options for Scriv. """ # The directory for fragments waiting to be collected. Also can have # templates and settings for scriv. fragment_directory = attr.ib( type=str, default="changelog.d", metadata={ "doc": """\ The directory for fragments. This directory must exist, it will not be created. """, }, ) # What format for fragments? reStructuredText ("rst") or Markdown ("md"). format = attr.ib( type=str, default="rst", validator=attr.validators.in_(["rst", "md"]), metadata={ "doc": """\ The format to use for fragments and for the output changelog file. Can be either "rst" or "md". """, }, ) # The categories for changelog fragments. Can be empty for no # categorization. categories = attr.ib( type=list, default=[ "Removed", "Added", "Changed", "Deprecated", "Fixed", "Security", ], metadata={ "doc": """\ Categories to use as headings for changelog items. See :ref:`categories`. """, }, ) output_file = attr.ib( type=str, default="CHANGELOG.${config:format}", metadata={ "doc": """\ The changelog file updated by ":ref:`cmd_collect`". """, }, ) insert_marker = attr.ib( type=str, default="scriv-insert-here", metadata={ "doc": """\ A marker string indicating where in the changelog file new entries should be inserted. """, }, ) end_marker = attr.ib( type=str, default="scriv-end-here", metadata={ "doc": """\ A marker string indicating where in the changelog file the changelog ends. """, }, ) # The characters to use for header and section underlines in rst files. rst_header_chars = attr.ib( type=str, default="=-", validator=attr.validators.matches_re(r"\S\S"), metadata={ "doc": """\ Two characters: for reStructuredText changelog files, these are the two underline characters to use. The first is for the heading for each changelog entry, the second is for the category sections within the entry. """, }, ) # What header level to use for markdown changelog entries? md_header_level = attr.ib( type=str, default="1", validator=attr.validators.matches_re(r"[123456]"), metadata={ "doc": """\ A number: for Markdown changelog files, this is the heading level to use for the entry heading. """, }, ) # The name of the template for new fragments. new_fragment_template = attr.ib( type=str, default="file: new_fragment.${config:format}.j2", metadata={ "doc": """\ The `Jinja`_ template to use for new fragments. """, }, ) # The template for the title of the changelog entry. entry_title_template = attr.ib( type=str, default=( "{% if version %}{{ version }} — {% endif %}" + "{{ date.strftime('%Y-%m-%d') }}" ), metadata={ "doc": """\ The `Jinja`_ template to use for the entry heading text for changelog entries created by ":ref:`cmd_collect`". """, }, ) # The version string to include in the title if wanted. version = attr.ib( type=str, default="", metadata={ "doc": """\ The string to use as the version number in the next header created by ``scriv collect``. Often, this will be a ``literal:`` directive, to get the version from a string in a source file. """, "doc_default": "(empty)", }, ) # Branches that aren't interesting enough to use in fragment file names. main_branches = attr.ib( type=list, default=["master", "main", "develop"], metadata={ "doc": """\ The branch names considered uninteresting to use in new fragment file names. """, }, ) # Glob for files in the fragments directory that should not be collected. skip_fragments = attr.ib( type=str, default="README.*", metadata={ "doc": """\ A glob pattern for files in the fragment directory that should not be collected. """, }, ) # Template for GitHub releases ghrel_template = attr.ib( type=str, default="{{body}}", metadata={ "doc": """\ The template to use for GitHub releases created by the ``scriv github-release`` command. The extracted Markdown text is available as ``{{body}}``. You must include this to use the text from the changelog file. The version is available as ``{{version}}``. The data for the release is available in a ``{{release}}`` object, including ``{{release.prerelease}}``. It's a boolean, true if this is a pre-release version. The scriv configuration is available in a ``{{config}}`` object. """, }, ) @contextlib.contextmanager def validator_exceptions(): """ Context manager for attrs operations that validate. Attrs >= 22 says ValueError will have a bunch of arguments, and we only want to see the first, and raised as ScrivException. """ try: yield except ValueError as ve: raise ScrivException(f"Invalid configuration: {ve.args[0]}") from ve class Config: """ Configuration for Scriv. All the settable options for Scriv, with resolution of values within other values. """ def __init__(self, **kwargs): """All values in _Options can be set as keywords.""" with validator_exceptions(): self._options = _Options(**kwargs) def __getattr__(self, name): """Proxy to self._options, and resolve the value.""" fields = attr.fields_dict(_Options) if name not in fields: raise AttributeError(f"Scriv configuration has no {name!r} option") attrdef = fields[name] value = getattr(self._options, name) if attrdef.type is list: if isinstance(value, str): value = convert_list(value) else: try: value = self.resolve_value(value) except ScrivException as se: raise ScrivException( f"Couldn't read {name!r} setting: {se}" ) from se setattr(self, name, value) return value @classmethod def read(cls) -> "Config": """ Read the configuration to use. Configuration will be read from setup.cfg, tox.ini, or changelog.d/scriv.ini. If setup.cfg or tox.ini defines a new fragment_directory, then scriv.ini is read from there. The section can be named ``[scriv]`` or ``[tool.scriv]``. """ config = cls() config.read_one_config("setup.cfg") config.read_one_config("tox.ini") config.read_one_toml("pyproject.toml") config.read_one_config( str(Path(config.fragment_directory) / "scriv.ini") ) with validator_exceptions(): attr.validate(config._options) return config def read_one_config(self, configfile: str) -> None: """ Read one configuration file, adding values to `self`. """ logger.debug(f"Looking for config file {configfile}") parser = configparser.ConfigParser() files_read = parser.read(configfile) if not files_read: logger.debug(f"{configfile} doesn't exist") return logger.debug(f"{configfile} was read") section_names = ["scriv", "tool.scriv"] section_name = next( (name for name in section_names if parser.has_section(name)), None ) if section_name: for attrdef in attr.fields(_Options): try: val: Any = parser[section_name][attrdef.name] except KeyError: pass else: setattr(self._options, attrdef.name, val) def read_one_toml(self, tomlfile: str) -> None: """ Read one .toml file if it exists, adding values to `self`. """ logger.debug(f"Looking for config file {tomlfile}") tomlpath = Path(tomlfile) if not tomlpath.exists(): logger.debug(f"{tomlfile} doesn't exist") return toml_text = tomlpath.read_text(encoding="utf-8") logger.debug(f"{tomlfile} was read") if tomllib is None: # Toml support isn't installed. Only print an exception if the # config file seems to have settings for us. has_scriv = re.search(r"(?m)^\[tool\.scriv\]", toml_text) if has_scriv: msg = ( "Can't read {!r} without TOML support. " + "Install with [toml] extra" ).format(tomlfile) raise ScrivException(msg) else: # We have toml installed, parse the file and look for our settings. data = tomllib.loads(toml_text) try: scriv_data = data["tool"]["scriv"] except KeyError: # No settings for us return for attrdef in attr.fields(_Options): try: val = scriv_data[attrdef.name] except KeyError: pass else: setattr(self._options, attrdef.name, val) def resolve_value(self, value: str) -> str: """ Interpret prefixes in config files to find the actual value. Also, "${config:format}" is replaced with the configured format ("rst" or "md"). Prefixes: "file:" read the content from a file. "literal:" read a literal string from a file. "command:" read the output of a shell command. """ value = value.replace("${config:format}", self._options.format) if value.startswith("file:"): file_name = value.partition(":")[2].strip() value = self.read_file_value(file_name) elif value.startswith("literal:"): try: _, file_name, literal_name = value.split(":", maxsplit=2) except ValueError as ve: raise ScrivException(f"Missing value name: {value!r}") from ve file_name = file_name.strip() if not file_name: raise ScrivException(f"Missing file name: {value!r}") literal_name = literal_name.strip() if not literal_name: raise ScrivException(f"Missing value name: {value!r}") try: found = find_literal(file_name, literal_name) except Exception as exc: raise ScrivException( f"Couldn't find literal {value!r}: {exc}" ) from exc if found is None: raise ScrivException( f"Couldn't find literal {literal_name!r} in {file_name}: " + f"{value!r}" ) value = found elif value.startswith("command:"): cmd = value.partition(":")[2].strip() ok, out = run_shell_command(cmd) if not ok: raise ScrivException(f"Command {cmd!r} failed:\n{out}") if out.count("\n") == 1: out = out.rstrip("\r\n") value = out return value def read_file_value(self, file_name: str) -> str: """ Find the value of a setting that has been specified as a file name. """ value = None possibilities = [] if not re.match(r"\.\.?[/\\]", file_name): possibilities.append(Path(self.fragment_directory) / file_name) possibilities.append(Path(".") / file_name) for file_path in possibilities: if file_path.exists(): value = file_path.read_text() break else: # No path, and doesn't exist: try it as a built-in. try: file_bytes = pkgutil.get_data("scriv", "templates/" + file_name) except OSError: pass else: assert file_bytes value = file_bytes.decode("utf-8") if value is None: raise ScrivException(f"No such file: {file_name}") return value def convert_list(val: str) -> List[str]: """ Convert a string value from a config into a list of strings. Elements can be separated by commas or newlines. """ vals = re.split(r"[\n,]", val) vals = [v.strip() for v in vals] vals = [v for v in vals if v] return vals scriv-1.4.0/src/scriv/create.py000066400000000000000000000026251451174641100164050ustar00rootroot00000000000000"""Creating fragments.""" import logging import sys from typing import Optional import click import click_log from .gitinfo import git_add, git_config_bool, git_edit from .scriv import Scriv logger = logging.getLogger(__name__) @click.command() @click.option( "--add/--no-add", default=None, help="'git add' the created file." ) @click.option( "--edit/--no-edit", default=None, help="Open the created file in your text editor.", ) @click_log.simple_verbosity_option() def create(add: Optional[bool], edit: Optional[bool]) -> None: """ Create a new changelog fragment. """ if add is None: add = git_config_bool("scriv.create.add") if edit is None: edit = git_config_bool("scriv.create.edit") scriv = Scriv() frag = scriv.new_fragment() file_path = frag.path if not file_path.parent.exists(): sys.exit( f"Output directory {str(file_path.parent)!r} doesn't exist," + " please create it." ) if file_path.exists(): sys.exit(f"File {file_path} already exists, not overwriting") logger.info(f"Creating {file_path}") frag.write() if edit: git_edit(file_path) sections = scriv.sections_from_fragment(frag) if not sections: logger.info("Empty fragment, aborting...") file_path.unlink() sys.exit() if add: git_add(file_path) scriv-1.4.0/src/scriv/exceptions.py000066400000000000000000000001641451174641100173170ustar00rootroot00000000000000"""Specialized exceptions for scriv.""" class ScrivException(Exception): """Any exception raised by scriv.""" scriv-1.4.0/src/scriv/format.py000066400000000000000000000040111451174641100164210ustar00rootroot00000000000000"""Dispatcher for format-based knowledge.""" import abc from typing import Dict, List, Optional from .config import Config # When collecting changelog fragments, we group them by their category into # Sections. A SectionDict maps category names to a list of the paragraphs in # that section. For projects not using categories, the key will be None. SectionDict = Dict[Optional[str], List[str]] class FormatTools(abc.ABC): """Methods and data about specific formats.""" def __init__(self, config: Optional[Config] = None): """Create a FormatTools with the specified configuration.""" self.config = config or Config() @abc.abstractmethod def parse_text(self, text: str) -> SectionDict: """ Parse text to find sections. Args: text: the marked-up text. Returns: A dict mapping section headers to a list of the paragraphs in each section. """ @abc.abstractmethod def format_header(self, text: str, anchor: Optional[str] = None) -> str: """ Format the header for a new changelog entry. """ @abc.abstractmethod def format_sections(self, sections: SectionDict) -> str: """ Format a series of sections into marked-up text. """ @abc.abstractmethod def convert_to_markdown(self, text: str) -> str: """ Convert this format to Markdown. """ def get_format_tools(fmt: str, config: Config) -> FormatTools: """ Return the FormatTools to use. Args: fmt: One of the supported formats ("rst" or "md"). config: The configuration settings to use. """ if fmt == "rst": from . import ( # pylint: disable=cyclic-import,import-outside-toplevel format_rst, ) return format_rst.RstTools(config) else: assert fmt == "md" from . import ( # pylint: disable=cyclic-import,import-outside-toplevel format_md, ) return format_md.MdTools(config) scriv-1.4.0/src/scriv/format_md.py000066400000000000000000000062651451174641100171160ustar00rootroot00000000000000"""Markdown text knowledge for scriv.""" import re from typing import Optional from .format import FormatTools, SectionDict class MdTools(FormatTools): """Specifics about how to work with Markdown.""" def parse_text( self, text ) -> SectionDict: # noqa: D102 (inherited docstring) lines = text.splitlines() # If there's an insert marker, start there. for lineno, line in enumerate(lines): if self.config.insert_marker in line: lines = lines[lineno + 1 :] break sections: SectionDict = {} in_comment = False paragraphs = None section_mark = None for line in lines: line = line.rstrip() if in_comment: if re.search(r"-->$", line): in_comment = False else: if re.search(r"^\s*$", line): # A one-line comment, skip it. continue if re.search(r"""^$""", line): # An anchor, we don't need those. continue if re.search(r"^\s* {% for cat in config.categories -%} {% endfor -%} scriv-1.4.0/src/scriv/templates/new_fragment.rst.j2000066400000000000000000000005611451174641100223030ustar00rootroot00000000000000.. A new scriv changelog fragment. {% if config.categories -%} .. .. Uncomment the header that is right (remove the leading dots). .. {% for cat in config.categories -%} .. {{ cat }} .. {{ config.rst_header_chars[1] * (cat|length) }} .. .. - A bullet item for the {{ cat }} category. .. {% endfor -%} {% else %} - A bullet item for this fragment. EDIT ME! {% endif -%} scriv-1.4.0/src/scriv/util.py000066400000000000000000000055221451174641100161160ustar00rootroot00000000000000"""Miscellanous helpers.""" from __future__ import annotations import collections import re from typing import Dict, Optional, Sequence, Tuple, TypeVar T = TypeVar("T") K = TypeVar("K") def order_dict( d: Dict[Optional[K], T], keys: Sequence[Optional[K]] ) -> Dict[Optional[K], T]: """ Produce an OrderedDict of `d`, but with the keys in `keys` order. Keys in `d` that don't appear in `keys` will be at the end in an undetermined order. """ with_order = collections.OrderedDict() to_insert = set(d) for k in keys: if k not in to_insert: continue with_order[k] = d[k] to_insert.remove(k) for k in to_insert: with_order[k] = d[k] return with_order def partition_lines(text: str, marker: str) -> Tuple[str, str, str]: """ Split `text` by lines, similar to str.partition. The splitting line is the first line containing `marker`. """ lines = text.splitlines(keepends=True) marker_pos = [i for i, line in enumerate(lines) if marker in line] if not marker_pos: return (text, "", "") pos = marker_pos[0] return ( "".join(lines[:pos]), lines[pos], "".join(lines[pos + 1 :]), ) VERSION_REGEX = r"""(?ix) # based on https://peps.python.org/pep-0440/ \b # at a word boundary v? # maybe a leading "v" (\d+!)? # maybe a version epoch \d+(\.\d+)+ # the meat of the version number: N.N.N (?P
        [-._]?[a-z]+\.?\d*
    )?                      # maybe a pre-release: .beta3
    ([-._][a-z]+\d*)*       # maybe post and dev releases
    (\+\w[\w.]*\w)?         # maybe a local version
    \b
    """


class Version:
    """
    A version string that compares correctly.

    For example, "v1.2.3" and "1.2.3" are considered the same.

    """

    def __init__(self, vtext: str) -> None:
        """Create a smart version from a string version number."""
        self.vtext = vtext

    def __repr__(self):
        return f""

    def __str__(self):
        return self.vtext

    def __bool__(self):
        return bool(self.vtext)

    def __eq__(self, other):
        assert isinstance(other, Version)
        return self.vtext.lstrip("v") == other.vtext.lstrip("v")

    def __hash__(self):
        return hash(self.vtext.lstrip("v"))

    @classmethod
    def from_text(cls, text: str) -> Optional[Version]:
        """Find a version number in a text string."""
        m = re.search(VERSION_REGEX, text)
        if m:
            return cls(m[0])
        return None

    def is_prerelease(self) -> bool:  # noqa: D400
        """Is this version number a pre-release?"""
        m = re.fullmatch(VERSION_REGEX, self.vtext)
        assert m  # the version must be a valid version
        return bool(m["pre"])
scriv-1.4.0/tests/000077500000000000000000000000001451174641100140105ustar00rootroot00000000000000scriv-1.4.0/tests/__init__.py000066400000000000000000000000331451174641100161150ustar00rootroot00000000000000"""The tests for scriv."""
scriv-1.4.0/tests/conftest.py000066400000000000000000000044501451174641100162120ustar00rootroot00000000000000"""Fixture definitions."""

import os
import sys
import traceback
from pathlib import Path
from typing import Iterable

import pytest
import responses
from click.testing import CliRunner

# We want to be able to test scriv without any extras installed.  But responses
# installs PyYaml.  If we are testing the no-extras scenario, then: after we've
# imported responses above, and before we import any scriv modules below,
# clobber the yaml module so that scriv's import will fail, simulating PyYaml
# not being available.
if os.getenv("SCRIV_TEST_NO_EXTRAS", ""):
    sys.modules["yaml"] = None  # type: ignore[assignment]

# pylint: disable=wrong-import-position

from scriv.cli import cli as scriv_cli

from .faker import FakeGit, FakeRunCommand


@pytest.fixture()
def fake_run_command(mocker):
    """Replace gitinfo.run_command with a fake."""
    return FakeRunCommand(mocker)


@pytest.fixture()
def fake_git(fake_run_command) -> FakeGit:
    """Get a FakeGit to use in tests."""
    return FakeGit(fake_run_command)


@pytest.fixture()
def temp_dir(tmpdir) -> Iterable[Path]:
    """Make and change into the tmpdir directory, as a Path."""
    old_dir = os.getcwd()
    tmpdir.chdir()
    try:
        yield Path(str(tmpdir))
    finally:
        os.chdir(old_dir)


@pytest.fixture()
def cli_invoke(temp_dir: Path):
    """
    Produce a function to invoke the Scriv cli with click.CliRunner.

    The test will run in a temp directory.
    """

    def invoke(command, expect_ok=True):
        runner = CliRunner()
        result = runner.invoke(scriv_cli, command)
        print(result.output)
        if result.exception:
            traceback.print_exception(
                None, result.exception, result.exception.__traceback__
            )
        if expect_ok:
            assert result.exception is None
            assert result.exit_code == 0
        return result

    return invoke


@pytest.fixture()
def changelog_d(temp_dir: Path) -> Path:
    """Make a changelog.d directory, and return a Path() to it."""
    the_changelog_d = temp_dir / "changelog.d"
    the_changelog_d.mkdir()
    return the_changelog_d


@pytest.fixture(autouse=True, name="responses")
def no_http_requests():
    """Activate `responses` for all tests, so no real HTTP happens."""
    with responses.RequestsMock() as rsps:
        yield rsps
scriv-1.4.0/tests/faker.py000066400000000000000000000074501451174641100154600ustar00rootroot00000000000000"""Fake implementations of some of our external information sources."""

import shlex
from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple

from scriv.shell import CmdResult

# A function that simulates run_command.
CmdHandler = Callable[[List[str]], CmdResult]


class FakeRunCommand:
    """
    A fake implementation of run_command.

    Add handlers for commands with `add_handler`.
    """

    def __init__(self, mocker):
        """Make the faker."""
        self.handlers: Dict[str, CmdHandler] = {}
        self.mocker = mocker
        self.patch_module("scriv.shell")

    def patch_module(self, mod_name: str) -> None:
        """Replace ``run_command`` in `mod_name` with our fake."""
        self.mocker.patch(f"{mod_name}.run_command", self)

    def add_handler(self, argv0: str, handler: CmdHandler) -> None:
        """
        Add a handler for a command.

        The first word of the command is `argv0`.  The handler will be called
        with the complete argv list.  It must return the same results that
        `run_command` would have returned.
        """
        self.handlers[argv0] = handler

    def __call__(self, cmd: str) -> CmdResult:
        """Do the faking!."""
        argv = shlex.split(cmd)
        if argv[0] in self.handlers:
            return self.handlers[argv[0]](argv)
        return (False, f"no fake command handler: {argv}")


class FakeGit:
    """Simulate aspects of our local Git."""

    def __init__(self, frc: FakeRunCommand) -> None:
        """Make a FakeGit from a FakeRunCommand."""
        # Initialize with basic defaults.
        self.config: Dict[str, str] = {
            "core.bare": "false",
            "core.repositoryformatversion": "0",
        }
        self.branch = "main"
        self.editor = "vi"
        self.tags: Set[str] = set()
        self.remotes: Dict[str, Tuple[str, str]] = {}

        # Hook up our run_command handler.
        frc.add_handler("git", self.run_command)

    def run_command(self, argv: List[str]) -> CmdResult:
        """Simulate git commands."""
        # todo: match/case someday
        if argv[1] == "config":
            if argv[2] == "--get":
                if argv[3] in self.config:
                    return (True, self.config[argv[3]] + "\n")
                else:
                    return (False, f"error: no such key: {argv[3]}")
        elif argv[1:] == ["rev-parse", "--abbrev-ref", "HEAD"]:
            return (True, self.branch + "\n")
        elif argv[1:] == ["tag"]:
            return (True, "".join(tag + "\n" for tag in self.tags))
        elif argv[1:] == ["var", "GIT_EDITOR"]:
            return (True, self.editor + "\n")
        elif argv[1:] == ["remote", "-v"]:
            out = []
            for name, (url, push_url) in self.remotes.items():
                out.append(f"{name}\t{url} (fetch)\n")
                out.append(f"{name}\t{push_url} (push)\n")
            return (True, "".join(out))
        return (False, f"no fake git command: {argv}")

    def set_config(self, name: str, value: str) -> None:
        """Set a fake Git configuration value."""
        self.config[name] = value

    def set_branch(self, branch_name: str) -> None:
        """Set the current fake branch."""
        self.branch = branch_name

    def set_editor(self, editor_name: str) -> None:
        """Set the name of the fake editor Git will launch."""
        self.editor = editor_name

    def add_tags(self, tags: Iterable[str]) -> None:
        """Add tags to the repo."""
        self.tags.update(tags)

    def add_remote(
        self, name: str, url: str, push_url: Optional[str] = None
    ) -> None:
        """Add a remote with a name and a url."""
        self.remotes[name] = (url, push_url or url)

    def remove_remote(self, name: str) -> None:
        """Remove the remote `name`."""
        del self.remotes[name]
scriv-1.4.0/tests/helpers.py000066400000000000000000000010561451174641100160260ustar00rootroot00000000000000"""Testing helpers."""

from unittest import mock


def without_module(using_module, missing_module_name: str):
    """
    Hide a module for testing.

    Use this in a test function to make an optional module unavailable during
    the test::

        with without_module(scriv.something, 'toml'):
            use_toml_somehow()

    Arguments:
        using_module: a module in which to hide `missing_module_name`.
        missing_module_name: the name of the module to hide.

    """
    return mock.patch.object(using_module, missing_module_name, None)
scriv-1.4.0/tests/test_changelog.py000066400000000000000000000025241451174641100173530ustar00rootroot00000000000000"""Tests of scriv/changelog.py"""

import pytest

from scriv.changelog import Changelog
from scriv.config import Config

A = """\
Hello
Goodbye
"""

B = """\
Now
more than
ever.
"""

BODY = """\
2022-09-13
==========

Added
-----

- Wrote tests for Changelog.

2022-02-25
==========

Added
-----

- Now you can send email with this tool.

Fixed
-----

- Launching missiles no longer targets ourselves.

- Typos corrected.
"""

BODY_SECTIONS = {
    "2022-09-13": [
        "Added\n-----",
        "- Wrote tests for Changelog.",
    ],
    "2022-02-25": [
        "Added\n-----",
        "- Now you can send email with this tool.",
        "Fixed\n-----",
        "- Launching missiles no longer targets ourselves.",
        "- Typos corrected.",
    ],
}


@pytest.mark.parametrize(
    "text",
    [
        BODY,
        ".. INSERT\n" + BODY,
        A + "(INSERT)\n" + BODY,
        A + "INSERT\n" + BODY + ".. END\n",
        A + ".. INSERT\n" + BODY + "(END)\n" + B,
        BODY + ".. END\n",
        BODY + ".. END\n" + B,
    ],
)
def test_round_trip(text, temp_dir):
    path = temp_dir / "foo.rst"
    config = Config(insert_marker="INSERT", end_marker="END")
    path.write_text(text)
    changelog = Changelog(path, config)
    changelog.read()
    assert changelog.entries() == BODY_SECTIONS
    changelog.write()
    assert path.read_text() == text
scriv-1.4.0/tests/test_collect.py000066400000000000000000000444301451174641100170530ustar00rootroot00000000000000"""Test collection logic."""

import textwrap
from unittest.mock import call

import freezegun
import pytest

COMMENT = """\
.. this line should be dropped

"""

COMMENT_MD = """\

"""

FRAG1 = """\
Fixed
-----

- Launching missiles no longer targets ourselves.
"""

FRAG2 = """\
Added
-----

- Now you can send email with this tool.

Fixed
-----

- Typos corrected.
"""

FRAG2_MD = """\
# Added

- Now you can send email with this tool.

# Fixed

- Typos corrected.
"""

FRAG3 = """\
Obsolete
--------

- This section has the wrong name.
"""

CHANGELOG_1_2 = """\

2020-02-25
==========

Added
-----

- Now you can send email with this tool.

Fixed
-----

- Launching missiles no longer targets ourselves.

- Typos corrected.
"""

CHANGELOG_2_1_3 = """\

2020-02-25
==========

Added
-----

- Now you can send email with this tool.

Fixed
-----

- Typos corrected.

- Launching missiles no longer targets ourselves.

Obsolete
--------

- This section has the wrong name.
"""

MARKED_CHANGELOG_A = """\
================
My Great Project
================

Blah blah.

Changes
=======

.. scriv-insert-here
"""

UNMARKED_CHANGELOG_B = """\

Other stuff
===========

Blah blah.
"""

CHANGELOG_HEADER = """\

2020-02-25
==========
"""


def test_collect_simple(cli_invoke, changelog_d, temp_dir):
    # Sections are ordered by the config file.
    # Fragments in sections are in time order.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "README.rst").write_text("This directory has fragments")
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG1 + COMMENT)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG2)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We didn't use --keep, so the files should be gone.
    assert (changelog_d / "scriv.ini").exists()
    assert not (changelog_d / "20170616_nedbat.rst").exists()
    assert not (changelog_d / "20170617_nedbat.rst").exists()


def test_collect_ordering(cli_invoke, changelog_d, temp_dir):
    # Fragments in sections are in time order.
    # Unknown sections come after the known ones.
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG2)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG1)
    (changelog_d / "20170618_joedev.rst").write_text(COMMENT + FRAG3)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_2_1_3


def test_collect_mixed_format(cli_invoke, changelog_d, temp_dir):
    # Fragments can be in mixed formats.
    (changelog_d / "README.md").write_text("Don't take this file.")
    (changelog_d / "20170616_nedbat.md").write_text(COMMENT_MD + FRAG2_MD)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG1)
    (changelog_d / "20170618_joedev.rst").write_text(COMMENT + FRAG3)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_2_1_3


def test_collect_inserts_at_marker(cli_invoke, changelog_d, temp_dir):
    # Collected text is inserted into CHANGELOG where marked.
    changelog = temp_dir / "CHANGELOG.rst"
    changelog.write_text(MARKED_CHANGELOG_A + UNMARKED_CHANGELOG_B)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = (
        MARKED_CHANGELOG_A
        + CHANGELOG_HEADER
        + "\n"
        + FRAG1
        + UNMARKED_CHANGELOG_B
    )
    assert changelog_text == expected


def test_collect_inserts_at_marker_no_header(cli_invoke, changelog_d, temp_dir):
    # No title this time.
    (changelog_d / "scriv.ini").write_text("[scriv]\nentry_title_template =\n")
    # Collected text is inserted into CHANGELOG where marked.
    changelog = temp_dir / "CHANGELOG.rst"
    changelog.write_text(MARKED_CHANGELOG_A + UNMARKED_CHANGELOG_B)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = MARKED_CHANGELOG_A + "\n" + FRAG1 + UNMARKED_CHANGELOG_B
    assert changelog_text == expected


def test_collect_prepends_if_no_marker(cli_invoke, changelog_d, temp_dir):
    # Collected text is inserted at the top of CHANGELOG if no marker.
    changelog = temp_dir / "CHANGELOG.rst"
    changelog.write_text(UNMARKED_CHANGELOG_B)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = CHANGELOG_HEADER + "\n" + FRAG1 + UNMARKED_CHANGELOG_B
    assert changelog_text == expected


def test_collect_keep(cli_invoke, changelog_d, temp_dir):
    # --keep tells collect to not delete the fragment files.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect", "--keep"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We used --keep, so the collected files should still exist.
    assert (changelog_d / "scriv.ini").exists()
    assert (changelog_d / "20170616_nedbat.rst").exists()
    assert (changelog_d / "20170617_nedbat.rst").exists()


def test_collect_no_categories(cli_invoke, changelog_d, temp_dir):
    # Categories can be empty, making a simpler changelog.
    changelog = temp_dir / "CHANGELOG.rst"
    (changelog_d / "scriv.ini").write_text("[scriv]\ncategories=\n")
    (changelog_d / "20170616_nedbat.rst").write_text("- The first change.\n")
    (changelog_d / "20170617_nedbat.rst").write_text(
        COMMENT + "- The second change.\n"
    )
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = (
        "\n"
        + "2020-02-25\n"
        + "==========\n\n"
        + "- The first change.\n\n"
        + "- The second change.\n"
    )
    assert changelog_text == expected


def test_collect_uncategorized_fragments(cli_invoke, changelog_d, temp_dir):
    # If using categories, even uncategorized fragments will be collected.
    changelog = temp_dir / "CHANGELOG.rst"
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text("- The second change.\n")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = "\n2020-02-25\n==========\n\n- The second change.\n\n" + FRAG1
    assert changelog_text == expected


def test_collect_add(mocker, cli_invoke, changelog_d, temp_dir):
    # --add tells collect to tell git what's going on.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    mock_call = mocker.patch("subprocess.call")
    mock_call.return_value = 0
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect", "--add"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We used --add, so the collected files were git rm'd
    assert mock_call.mock_calls == [
        call(["git", "add", "CHANGELOG.rst"]),
        call(
            [
                "git",
                "rm",
                str(
                    (changelog_d / "20170616_nedbat.rst").relative_to(temp_dir)
                ),
            ]
        ),
        call(
            [
                "git",
                "rm",
                str(
                    (changelog_d / "20170617_nedbat.rst").relative_to(temp_dir)
                ),
            ]
        ),
    ]


def test_collect_add_rm_fail(mocker, cli_invoke, changelog_d, temp_dir):
    # --add, but fail to remove a file.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    mock_call = mocker.patch("subprocess.call")
    mock_call.side_effect = [0, 99]
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        result = cli_invoke(["collect", "--add"], expect_ok=False)
    assert result.exit_code == 99
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We used --add, so the collected files were git rm'd
    assert mock_call.mock_calls == [
        call(["git", "add", "CHANGELOG.rst"]),
        call(
            [
                "git",
                "rm",
                str(
                    (changelog_d / "20170616_nedbat.rst").relative_to(temp_dir)
                ),
            ]
        ),
    ]


def test_collect_edit(fake_git, mocker, cli_invoke, changelog_d, temp_dir):
    # --edit tells collect to open the changelog in an editor.
    fake_git.set_editor("my_fav_editor")
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    mock_edit = mocker.patch("click.edit")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect", "--edit"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    mock_edit.assert_called_once_with(
        filename="CHANGELOG.rst", editor="my_fav_editor"
    )


def test_collect_version_in_config(cli_invoke, changelog_d, temp_dir):
    # The version number to use in the changelog entry can be specified in the
    # config file.
    changelog = temp_dir / "CHANGELOG.rst"
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = v12.34b\n")
    (changelog_d / "20170616_nedbat.rst").write_text("- The first change.\n")
    with freezegun.freeze_time("2020-02-26T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text(encoding="utf-8")
    expected = (
        "\n"
        + ".. _changelog-v12.34b:\n"
        + "\n"
        + "v12.34b — 2020-02-26\n"
        + "====================\n\n"
        + "- The first change.\n"
    )
    assert changelog_text == expected


@pytest.mark.parametrize(
    "platform, newline",
    (
        ("Windows", "\r\n"),
        ("Linux", "\n"),
        ("Mac", "\r"),
    ),
)
def test_collect_respect_existing_newlines(
    cli_invoke,
    changelog_d,
    temp_dir,
    platform,
    newline,
):
    """Verify that existing newline styles are preserved during collection."""

    index_map = {
        "\r\n": 0,
        "\n": 1,
        "\r": 2,
    }

    changelog = temp_dir / "CHANGELOG.rst"
    existing_text = "Line one" + newline + "Line two"
    with changelog.open("wb") as file:
        file.write(existing_text.encode("utf8"))
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG1 + COMMENT)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    with changelog.open("rb") as file:
        new_text = file.read().decode("utf8")

    counts = [new_text.count("\r\n")]  # Windows
    counts.append(new_text.count("\n") - counts[0])  # Linux
    counts.append(new_text.count("\r") - counts[0])  # Mac

    msg = platform + " newlines were not preserved"
    assert counts.pop(index_map[newline]), msg
    assert counts == [0, 0], msg


def test_no_newlines(cli_invoke, changelog_d, temp_dir):
    changelog = temp_dir / "CHANGELOG.rst"
    with changelog.open("wb") as file:
        file.write(b"no newline")
    (changelog_d / "20170616_nedbat.rst").write_text("A bare line")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    new_text = changelog.read_text()
    assert new_text == "\n2020-02-25\n==========\n\nA bare line\nno newline"


def test_mixed_newlines(cli_invoke, changelog_d, temp_dir):
    changelog = temp_dir / "CHANGELOG.rst"
    with changelog.open("wb") as file:
        file.write(b"slashr\rslashn\n")
    (changelog_d / "20170616_nedbat.rst").write_text("A bare line")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    new_text = changelog.read_text()
    assert (
        new_text == "\n2020-02-25\n==========\n\nA bare line\nslashr\nslashn\n"
    )


def test_configure_skipped_fragments(cli_invoke, changelog_d, temp_dir):
    # The skipped "readme" files can be configured.
    (changelog_d / "scriv.ini").write_text("[scriv]\nskip_fragments = ALL*\n")
    (changelog_d / "ALLABOUT.md").write_text("Don't take this file.")
    (changelog_d / "20170616_nedbat.md").write_text(COMMENT_MD + FRAG2_MD)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG1)
    (changelog_d / "20170618_joedev.rst").write_text(COMMENT + FRAG3)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_2_1_3


def test_no_fragments(cli_invoke, changelog_d, temp_dir, caplog):
    (changelog_d / "README.rst").write_text("This directory has fragments")
    (temp_dir / "CHANGELOG.rst").write_text("Not much\n")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 2
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == "Not much\n"
    assert "No changelog fragments to collect" in caplog.text


def test_title_provided(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG1 + COMMENT)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG2)
    title = "This is the Header"
    cli_invoke(["collect", "--title", title])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    # With --title provided, the first header is literally what was provided.
    lines = CHANGELOG_1_2.splitlines()
    lines[1] = title
    lines[2] = len(title) * "="
    assert changelog_text == "\n".join(lines) + "\n"


def test_title_and_version_clash(cli_invoke):
    result = cli_invoke(
        ["collect", "--title", "xx", "--version", "1.2"], expect_ok=False
    )
    assert result.exit_code == 1
    assert str(result.exception) == "Can't provide both --title and --version."


def test_duplicate_version(cli_invoke, changelog_d, temp_dir):
    (temp_dir / "foob.py").write_text(
        """# comment\n__version__ = "12.34.56"\n"""
    )
    (changelog_d / "scriv.ini").write_text(
        "[scriv]\nversion = literal:foob.py: __version__\n"
    )
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2022-09-18T15:18:19"):
        cli_invoke(["collect"])

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert (
        str(result.exception)
        == "Entry '12.34.56 — 2022-09-18' already uses version '12.34.56'."
    )


def test_duplicate_version_2(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = 12.34.56\n")
    (temp_dir / "CHANGELOG.rst").write_text(
        textwrap.dedent(
            """\
            Preamble that doesn't count

            12.34.57 — 2022-09-19
            =====================

            A quick fix.

            12.34.56 — 2022-09-18
            =====================

            Good stuff.
            """
        ),
        encoding="utf-8",
    )

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert (
        str(result.exception)
        == "Entry '12.34.56 — 2022-09-18' already uses version '12.34.56'."
    )


def test_duplicate_version_with_v(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = 12.34.56\n")
    (temp_dir / "CHANGELOG.rst").write_text(
        textwrap.dedent(
            """\
            Preamble that doesn't count

            v12.34.57 — 2022-09-19
            ======================

            A quick fix.

            v12.34.56 — 2022-09-18
            ======================

            Good stuff.
            """
        ),
        encoding="utf-8",
    )

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert (
        str(result.exception)
        == "Entry 'v12.34.56 — 2022-09-18' already uses version '12.34.56'."
    )


def test_unparsable_version(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = 12.34.56\n")
    (temp_dir / "CHANGELOG.rst").write_text(
        textwrap.dedent(
            """\
            Preamble that doesn't count

            v12.34.57 — 2022-09-19
            ======================

            A quick fix.

            Not a version at all.
            =====================

            Good stuff.
            """
        ),
        encoding="utf-8",
    )

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert str(result.exception) == (
        "Entry 'Not a version at all.' is not a valid version! "
        + "If scriv should ignore this heading, add "
        + "'scriv-end-here' somewhere before it."
    )
scriv-1.4.0/tests/test_config.py000066400000000000000000000270601451174641100166730ustar00rootroot00000000000000"""Tests of scriv/config.py"""

import re

import pytest

import scriv.config
from scriv.config import Config
from scriv.exceptions import ScrivException
from scriv.optional import tomllib

from .helpers import without_module

CONFIG1 = """\
[scriv]
output_file = README.md
categories = New, Different, Gone, Bad
"""

CONFIG2 = """\
[someotherthing]
no_idea = what this is

[tool.scriv]
output_file = README.md
categories =
    New
    Different
    Gone
    Bad

[more stuff]
value = 17
"""

GENERIC_TOML_CONFIG = """\
[project]
name = "spam"
version = "2020.0.0"
description = "Lovely Spam! Wonderful Spam!"
readme = "README.rst"
requires-python = ">=3.8"
license = {file = "LICENSE.txt"}
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
authors = [
  {email = "hi@pradyunsg.me"},
  {name = "Tzu-Ping Chung"}
]
maintainers = [
  {name = "Brett Cannon", email = "brett@python.org"}
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Programming Language :: Python"
]
"""

TOML_CONFIG = (
    GENERIC_TOML_CONFIG
    + """
[tool.scriv]
output_file = "README.md"
categories = [
    "New",
    "Different",
    "Gone",
    "Bad",
]

["more stuff"]
value = 17
"""
)


def test_defaults(temp_dir):
    # No configuration files anywhere, just get all the defaults.
    config = Config.read()
    assert config.fragment_directory == "changelog.d"
    assert config.format == "rst"
    assert config.new_fragment_template.startswith(
        ".. A new scriv changelog fragment"
    )
    assert config.categories == [
        "Removed",
        "Added",
        "Changed",
        "Deprecated",
        "Fixed",
        "Security",
    ]
    assert config.output_file == "CHANGELOG.rst"
    assert config.insert_marker == "scriv-insert-here"
    assert config.rst_header_chars == "=-"
    assert config.md_header_level == "1"
    assert "{{ date.strftime('%Y-%m-%d') }}" in config.entry_title_template
    assert config.main_branches == ["master", "main", "develop"]
    assert config.skip_fragments == "README.*"
    assert config.version == ""


def test_reading_config(temp_dir):
    (temp_dir / "setup.cfg").write_text(CONFIG1)
    config = Config.read()
    assert config.fragment_directory == "changelog.d"
    assert config.output_file == "README.md"
    assert config.categories == ["New", "Different", "Gone", "Bad"]


def test_reading_config_list(temp_dir):
    (temp_dir / "tox.ini").write_text(CONFIG2)
    config = Config.read()
    assert config.categories == ["New", "Different", "Gone", "Bad"]


def test_reading_config_from_directory(changelog_d):
    # The settings file can be changelog.d/scriv.ini .
    (changelog_d / "scriv.ini").write_text(CONFIG1)
    config = Config.read()
    assert config.categories == ["New", "Different", "Gone", "Bad"]


def test_reading_config_from_other_directory(temp_dir):
    # setup.cfg can set the fragment directory, and then scriv.ini will
    # be found there.
    (temp_dir / "scriv.d").mkdir()
    (temp_dir / "scriv.d" / "scriv.ini").write_text(CONFIG1)
    (temp_dir / "setup.cfg").write_text(
        "[tool.scriv]\nfragment_directory = scriv.d\n"
    )
    config = Config.read()
    assert config.fragment_directory == "scriv.d"
    assert config.categories == ["New", "Different", "Gone", "Bad"]


def test_unknown_option():
    config = Config.read()
    expected = "Scriv configuration has no 'foo' option"
    with pytest.raises(AttributeError, match=expected):
        _ = config.foo


def test_custom_template(changelog_d):
    # You can define your own template with your own name.
    (changelog_d / "start_here.j2").write_text("Custom template.")
    fmt = Config(
        new_fragment_template="file: start_here.j2"
    ).new_fragment_template
    assert fmt == "Custom template."


def test_file_with_dots(temp_dir, changelog_d):
    # A file: spec with dot components is relative to the current directory.
    (changelog_d / "start_here.j2").write_text("The wrong one")
    (temp_dir / "start_here.j2").write_text("The right one")
    fmt = Config(
        new_fragment_template="file: ./start_here.j2"
    ).new_fragment_template
    assert fmt == "The right one"


def test_file_with_path_search_order(temp_dir, changelog_d):
    # A file: spec with path components is relative to the changelog directory
    # and then the current directory.
    (changelog_d / "files").mkdir()
    (changelog_d / "files" / "start_here.j2").write_text("The right one")
    (temp_dir / "files").mkdir()
    (temp_dir / "files" / "start_here.j2").write_text("The wrong one")
    fmt = Config(
        new_fragment_template="file: files/start_here.j2"
    ).new_fragment_template
    assert fmt == "The right one"


def test_file_with_path_only_current_dir(temp_dir, changelog_d):
    # A file: spec with path components is relative to the changelog directory
    # and then the current directory.
    (temp_dir / "files").mkdir()
    (temp_dir / "files" / "start_here.j2").write_text("The right one")
    fmt = Config(
        new_fragment_template="file: files/start_here.j2"
    ).new_fragment_template
    assert fmt == "The right one"


def test_missing_file_with_path(temp_dir, changelog_d):
    # A file: spec with path components is relative to the current directory.
    (changelog_d / "start_here.j2").write_text("The wrong one")
    msg = (
        r"Couldn't read 'new_fragment_template' setting: "
        + r"No such file: there[/\\]start_here.j2"
    )
    with pytest.raises(ScrivException, match=msg):
        config = Config(new_fragment_template="file: there/start_here.j2")
        _ = config.new_fragment_template


def test_unknown_format():
    with pytest.raises(
        ScrivException,
        match=r"'format' must be in \['rst', 'md'\] \(got 'xyzzy'\)",
    ):
        Config(format="xyzzy")


def test_no_such_template():
    # If you specify a template name, and it doesn't exist, an error will
    # be raised.
    msg = (
        r"Couldn't read 'new_fragment_template' setting: "
        + r"No such file: foo\.j2"
    )
    with pytest.raises(ScrivException, match=msg):
        config = Config(new_fragment_template="file: foo.j2")
        _ = config.new_fragment_template


def test_override_default_name(changelog_d):
    # You can define a file named new_fragment.rst.j2, and it will be read
    # as the template.
    (changelog_d / "new_fragment.rst.j2").write_text("Hello there!")
    fmt = Config().new_fragment_template
    assert fmt == "Hello there!"


def test_file_reading(changelog_d):
    # Any setting can be read from a file, even where it doesn't make sense.
    (changelog_d / "hello.txt").write_text("Xyzzy")
    text = Config(output_file="file:hello.txt").output_file
    assert text == "Xyzzy"


def test_literal_reading(temp_dir):
    # Any setting can be read from a literal in a file.
    (temp_dir / "sub").mkdir()
    (temp_dir / "sub" / "foob.py").write_text(
        """# comment\n__version__ = "12.34.56"\n"""
    )
    text = Config(version="literal:sub/foob.py: __version__").version
    assert text == "12.34.56"


@pytest.mark.parametrize(
    "bad_spec, msg_rx",
    [
        (
            "literal: myproj.py",
            (
                r"Couldn't read 'version' setting: "
                + r"Missing value name: 'literal: myproj.py'"
            ),
        ),
        (
            "literal: myproj.py:",
            (
                r"Couldn't read 'version' setting: "
                + r"Missing value name: 'literal: myproj.py:'"
            ),
        ),
        (
            "literal: myproj.py:  version",
            (
                r"Couldn't read 'version' setting: "
                + r"Couldn't find literal 'version' in myproj.py: "
                + r"'literal: myproj.py:  version'"
            ),
        ),
        (
            "literal: : version",
            (
                r"Couldn't read 'version' setting: "
                + r"Missing file name: 'literal: : version'"
            ),
        ),
        (
            "literal: no_file.py: version",
            (
                r"Couldn't read 'version' setting: "
                + r"Couldn't find literal 'literal: no_file.py: version': "
                + r".* 'no_file.py'"
            ),
        ),
    ],
)
def test_bad_literal_spec(bad_spec, msg_rx, temp_dir):
    (temp_dir / "myproj.py").write_text("""nothing_to_see_here = 'hello'\n""")
    with pytest.raises(ScrivException, match=msg_rx):
        config = Config(version=bad_spec)
        _ = config.version


@pytest.mark.parametrize("chars", ["", "#", "#=-", "# ", "  "])
def test_rst_chars_is_two_chars(chars):
    # rst_header_chars must be exactly two non-space characters.
    msg = rf"Invalid configuration: 'rst_header_chars' must match.*'{chars}'"
    with pytest.raises(ScrivException, match=msg):
        Config(rst_header_chars=chars)


def test_md_format(changelog_d):
    (changelog_d / "scriv.ini").write_text("[scriv]\nformat = md\n")
    config = Config.read()
    assert config.output_file == "CHANGELOG.md"
    template = re.sub(r"\s+", " ", config.new_fragment_template)
    assert template.startswith("

            # Added

            - This thing was added.
              And we liked it.

            

            - Another thing we added.

            
            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Another thing we added.",
                ]
            },
            id="comments_ignored",
        ),
        # Multiple section headers.
        pytest.param(
            """\
            # Added

            - This thing was added.
              And we liked it.


            # Fixed

            - This thing was fixed.

            - Another thing was fixed.

            # Added

            - Also added
              this thing
              that is very important.

            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Also added\n  this thing\n  that is very important.",
                ],
                "Fixed": [
                    "- This thing was fixed.",
                    "- Another thing was fixed.",
                ],
            },
            id="multiple_headers",
        ),
        # Multiple section headers at a different level.
        pytest.param(
            """\
            ### Added

            - This thing was added.
              And we liked it.


            ###     Fixed or Something

            - This thing was fixed.

            - Another thing was fixed.

            ### Added

            - Also added
              this thing
              that is very important.

            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Also added\n  this thing\n  that is very important.",
                ],
                "Fixed or Something": [
                    "- This thing was fixed.",
                    "- Another thing was fixed.",
                ],
            },
            id="multiple_headers_2",
        ),
        # It's fine to have no header at all.
        pytest.param(
            """\
            - No header at all.
            """,
            {None: ["- No header at all."]},
            id="no_header",
        ),
        # It's fine to have comments with no header, and multiple bulllets.
        pytest.param(
            """\
            

            - No header at all.

            - Just plain bullets.
            """,
            {None: ["- No header at all.", "- Just plain bullets."]},
            id="no_header_2",
        ),
        # A file with only comments and blanks will produce nothing.
        pytest.param(
            """\
            

            


            """,
            {},
            id="empty",
        ),
        # Multiple levels of headings only splits on the top-most one.
        pytest.param(
            """\
            # The big title

            Ignore this stuff

            

            (prelude)

            
            ## Section one

            ### subhead

            In the sub

            ### subhead 2

            Also sub

            
            ## Section two

            In section two.

            ### subhead 3
            s2s3
            """,
            {
                None: ["(prelude)"],
                "Section one": [
                    "### subhead",
                    "In the sub",
                    "### subhead 2",
                    "Also sub",
                ],
                "Section two": [
                    "In section two.",
                    "### subhead 3\ns2s3",
                ],
            },
            id="multilevel",
        ),
    ],
)
def test_parse_text(text, parsed):
    actual = MdTools().parse_text(textwrap.dedent(text))
    assert actual == parsed


@pytest.mark.parametrize(
    "sections, expected",
    [
        pytest.param(
            [
                (
                    "Added",
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
                (
                    "Fixed",
                    ["- This thing was fixed.", "- Another thing was fixed."],
                ),
            ],
            """\

            ### Added

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.

            ### Fixed

            - This thing was fixed.

            - Another thing was fixed.
            """,
            id="one",
        ),
        pytest.param(
            [
                (
                    None,
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
            ],
            """\

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.
            """,
            id="two",
        ),
    ],
)
def test_format_sections(sections, expected):
    sections = collections.OrderedDict(sections)
    actual = MdTools(Config(md_header_level="2")).format_sections(sections)
    assert actual == textwrap.dedent(expected)


@pytest.mark.parametrize(
    "config_kwargs, text, fh_kwargs, result",
    [
        ({}, "2020-07-26", {}, "\n# 2020-07-26\n"),
        ({"md_header_level": "3"}, "2020-07-26", {}, "\n### 2020-07-26\n"),
        (
            {},
            "2022-04-03",
            {"anchor": "here"},
            "\n\n# 2022-04-03\n",
        ),
    ],
)
def test_format_header(config_kwargs, text, fh_kwargs, result):
    actual = MdTools(Config(**config_kwargs)).format_header(text, **fh_kwargs)
    assert actual == result


def test_convert_to_markdown():
    # Markdown's convert_to_markdown is a no-op.
    md = "# Nonsense\ndoesn't matter\n- whatever ``more ** junk---"
    converted = MdTools().convert_to_markdown(md)
    assert converted == md
scriv-1.4.0/tests/test_format_rst.py000066400000000000000000000222771451174641100176130ustar00rootroot00000000000000"""Tests for scriv/format_rst.py."""

import collections
import re
import textwrap

import pytest

from scriv.config import Config
from scriv.exceptions import ScrivException
from scriv.format_rst import RstTools


@pytest.mark.parametrize(
    "text, parsed",
    [
        # Comments are ignored, and the section headers found.
        pytest.param(
            """\
            .. Comments can be here
            .. and here.
            ..
            .. and here.
            Added
            -----

            - This thing was added.
              And we liked it.

            .. More comments can be here
            ..
            .. And here.

            """,
            {"Added": ["- This thing was added.\n  And we liked it."]},
            id="comments_ignored",
        ),
        # Multiple section headers.
        pytest.param(
            """\
            Added
            -----

            - This thing was added.
              And we liked it.


            Fixed
            -----

            - This thing was fixed.

            - Another thing was fixed.

            Added
            -----

            - Also added
              this thing
              that is very important.

            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Also added\n  this thing\n  that is very important.",
                ],
                "Fixed": [
                    "- This thing was fixed.",
                    "- Another thing was fixed.",
                ],
            },
            id="multiple_headers",
        ),
        # The specific character used for the header line is unimportant.
        pytest.param(
            """\
            Added
            ^^^^^
            - This thing was added.

            Fixed
            ^^^^^
            - This thing was fixed.
            """,
            {
                "Added": ["- This thing was added."],
                "Fixed": ["- This thing was fixed."],
            },
            id="different_underlines",
        ),
        # You can even use periods as the underline, it won't be confused for a
        # comment.
        pytest.param(
            """\
            Fixed
            .....
            - This thing was fixed.

            Added
            .....

            .. a comment.

            - This thing was added.
            """,
            {
                "Added": ["- This thing was added."],
                "Fixed": ["- This thing was fixed."],
            },
            id="period_underline",
        ),
        # It's fine to have no header at all.
        pytest.param(
            """\
            - No header at all.
            """,
            {None: ["- No header at all."]},
            id="no_header",
        ),
        # It's fine to have comments with no header, and multiple bulllets.
        pytest.param(
            """\
            .. This is a scriv fragment.

            - No header at all.

            - Just plain bullets.
            """,
            {None: ["- No header at all.", "- Just plain bullets."]},
            id="no_header_2",
        ),
        # RST syntax is intricate. We understand a subset of it.
        pytest.param(
            """\
            .. _fixed.1:

            Fixed
            .....
            - This thing was fixed: `issue 42`_.

            .. _issue 42: https://github.com/thing/issue/42

            .. _added:

            Added
            .....

            .. a comment.

            - This thing was added.

            .. note::
                This thing doesn't work yet.
                Not sure it ever will... :(

            .. [cite] A citation

            .. |subst| image:: substitution.png

            ..

            """,
            {
                "Added": [
                    "- This thing was added.",
                    (
                        ".. note::\n"
                        + "    This thing doesn't work yet.\n"
                        + "    Not sure it ever will... :("
                    ),
                    ".. [cite] A citation",
                    ".. |subst| image:: substitution.png",
                ],
                "Fixed": [
                    "- This thing was fixed: `issue 42`_.",
                    ".. _issue 42: https://github.com/thing/issue/42",
                ],
            },
            id="intricate_syntax",
        ),
        # A file with only comments and blanks will produce nothing.
        pytest.param(
            """\
            .. Nothing to see here.
            ..

            .. Or here.


            """,
            {},
            id="empty",
        ),
        # Multiple levels of headings only splits on the top-most one.
        pytest.param(
            """\
            =====
            TITLE
            =====

            Irrelevant stuff

            Heading
            =======

            Ignore this

            .. scriv-insert-here

            (prelude)

            ===========
            Section one
            ===========

            subhead
            -------

            In the sub

            subhead 2
            ---------

            Also sub

            Section two
            ===========

            In section two.

            subhead 3
            ---------
            s2s3
            """,
            {
                None: ["(prelude)"],
                "Section one": [
                    "subhead\n-------",
                    "In the sub",
                    "subhead 2\n---------",
                    "Also sub",
                ],
                "Section two": [
                    "In section two.",
                    "subhead 3\n---------\ns2s3",
                ],
            },
            id="multilevel",
        ),
    ],
)
def test_parse_text(text, parsed):
    actual = RstTools().parse_text(textwrap.dedent(text))
    assert actual == parsed


@pytest.mark.parametrize(
    "sections, expected",
    [
        pytest.param(
            [
                (
                    "Added",
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
                (
                    "Fixed",
                    ["- This thing was fixed.", "- Another thing was fixed."],
                ),
            ],
            """\

            Added
            ~~~~~

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.

            Fixed
            ~~~~~

            - This thing was fixed.

            - Another thing was fixed.
            """,
            id="one",
        ),
        pytest.param(
            [
                (
                    None,
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
            ],
            """\

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.
            """,
            id="two",
        ),
    ],
)
def test_format_sections(sections, expected):
    sections = collections.OrderedDict(sections)
    actual = RstTools(Config(rst_header_chars="#~")).format_sections(sections)
    assert actual == textwrap.dedent(expected)


@pytest.mark.parametrize(
    "config_kwargs, text, fh_kwargs, result",
    [
        ({}, "2020-07-26", {}, "\n2020-07-26\n==========\n"),
        (
            {"rst_header_chars": "*-"},
            "2020-07-26",
            {},
            "\n2020-07-26\n**********\n",
        ),
        (
            {},
            "2022-04-03",
            {"anchor": "here"},
            "\n.. _here:\n\n2022-04-03\n==========\n",
        ),
    ],
)
def test_format_header(config_kwargs, text, fh_kwargs, result):
    actual = RstTools(Config(**config_kwargs)).format_header(text, **fh_kwargs)
    assert actual == result


def test_fake_pandoc(fake_run_command):
    fake_run_command.patch_module("scriv.format_rst")
    expected_args = [
        "pandoc",
        "-frst",
        "-tmarkdown_strict",
        "--markdown-headings=atx",
        "--wrap=none",
    ]
    expected_text = "The converted text!\nis multi-line\n"

    def fake_pandoc(argv):
        # We got the arguments we expected, plus one more.
        assert argv[: len(expected_args)] == expected_args
        assert len(argv) == len(expected_args) + 1
        return (True, expected_text)

    fake_run_command.add_handler("pandoc", fake_pandoc)
    assert RstTools().convert_to_markdown("Hello") == expected_text


def test_fake_pandoc_failing(fake_run_command):
    fake_run_command.patch_module("scriv.format_rst")
    error_text = "There was a problem!!?!"

    def fake_pandoc(argv):  # pylint: disable=unused-argument
        return (False, error_text)

    fake_run_command.add_handler("pandoc", fake_pandoc)
    expected = f"Couldn't convert ReST to Markdown: {error_text!r}"
    with pytest.raises(ScrivException, match=re.escape(expected)):
        _ = RstTools().convert_to_markdown("Hello")
scriv-1.4.0/tests/test_ghrel.py000066400000000000000000000163351451174641100165320ustar00rootroot00000000000000"""Tests of scriv/ghrel.py."""

import json
import logging
from typing import Any, Dict
from unittest.mock import call

import pytest

CHANGELOG1 = """\
Some text before

v1.2.3 -- 2022-04-21
--------------------

A good release

Some fixes
----------

No version number in this section.

v1.0 -- 2020-02-20
------------------

Nothing to say.

v0.9a7 -- 2017-06-16
--------------------

A beginning

v0.1 -- 2010-01-01
------------------

Didn't bother to tag this one.

v0.0.1 -- 2001-01-01
--------------------

Very first.

"""

RELEASES1 = {
    "v1.0": {
        "url": "https://api.github.com/repos/joe/project/releases/120",
        "body": "Nothing to say.\n",
    },
    "v0.9a7": {
        "url": "https://api.github.com/repos/joe/project/releases/123",
        "body": "original body",
    },
    "v0.0.1": {
        "url": "https://api.github.com/repos/joe/project/releases/123",
        "body": "original body",
    },
}

V123_REL = {
    "body": "A good release\n",
    "name": "v1.2.3",
    "tag_name": "v1.2.3",
    "draft": False,
    "prerelease": False,
}

V097_REL = {
    "body": "A beginning\n",
    "name": "v0.9a7",
    "tag_name": "v0.9a7",
    "draft": False,
    "prerelease": True,
}

V001_REL = {
    "body": "Very first.\n",
    "name": "v0.0.1",
    "tag_name": "v0.0.1",
    "draft": False,
    "prerelease": False,
}


@pytest.fixture()
def scenario1(temp_dir, fake_git, mocker):
    """A common scenario for the tests."""
    fake_git.add_remote("origin", "git@github.com:joe/project.git")
    fake_git.add_tags(["v1.2.3", "v1.0", "v0.9a7", "v0.0.1"])
    (temp_dir / "CHANGELOG.rst").write_text(CHANGELOG1)
    mock_get_releases = mocker.patch("scriv.ghrel.get_releases")
    mock_get_releases.return_value = RELEASES1


@pytest.fixture()
def mock_create_release(mocker):
    """Create a mock create_release that checks arguments."""

    def _create_release(repo: str, release_data: Dict[str, Any]) -> None:
        assert repo
        assert release_data["name"]
        assert json.dumps(release_data)[0] == "{"

    return mocker.patch(
        "scriv.ghrel.create_release", side_effect=_create_release
    )


@pytest.fixture()
def mock_update_release(mocker):
    """Create a mock update_release that checks arguments."""

    def _update_release(
        release: Dict[str, Any], release_data: Dict[str, Any]
    ) -> None:
        assert release_data["name"]
        assert release["url"]
        assert json.dumps(release_data)[0] == "{"

    return mocker.patch(
        "scriv.ghrel.update_release", side_effect=_update_release
    )


def test_default(
    cli_invoke, scenario1, mock_create_release, mock_update_release, caplog
):
    cli_invoke(["github-release"])

    assert mock_create_release.mock_calls == [call("joe/project", V123_REL)]
    assert mock_update_release.mock_calls == []
    assert caplog.record_tuples == [
        (
            "scriv.changelog",
            logging.INFO,
            "Reading changelog CHANGELOG.rst",
        ),
    ]


def test_dash_all(
    cli_invoke, scenario1, mock_create_release, mock_update_release, caplog
):
    cli_invoke(["github-release", "--all"])

    assert mock_create_release.mock_calls == [call("joe/project", V123_REL)]
    assert mock_update_release.mock_calls == [
        call(RELEASES1["v0.9a7"], V097_REL),
        call(RELEASES1["v0.0.1"], V001_REL),
    ]
    assert caplog.record_tuples == [
        (
            "scriv.changelog",
            logging.INFO,
            "Reading changelog CHANGELOG.rst",
        ),
        (
            "scriv.ghrel",
            logging.WARNING,
            "Entry 'Some fixes' has no version, skipping.",
        ),
        (
            "scriv.ghrel",
            logging.WARNING,
            "Version v0.1 has no tag. No release will be made.",
        ),
    ]


def test_explicit_repo(
    cli_invoke, scenario1, fake_git, mock_create_release, mock_update_release
):
    # Add another GitHub remote, now there are two.
    fake_git.add_remote("upstream", "git@github.com:psf/project.git")

    cli_invoke(["github-release", "--repo=xyzzy/plugh"])

    assert mock_create_release.mock_calls == [call("xyzzy/plugh", V123_REL)]
    assert mock_update_release.mock_calls == []


@pytest.mark.parametrize(
    "repo", ["xyzzy", "https://github.com/xyzzy/plugh.git"]
)
def test_bad_explicit_repo(cli_invoke, repo):
    result = cli_invoke(["github-release", f"--repo={repo}"], expect_ok=False)
    assert result.exit_code == 1
    assert str(result.exception) == f"Repo must be owner/reponame: {repo!r}"


@pytest.fixture()
def no_actions(mock_create_release, mock_update_release, responses):
    """Check that nothing really happened."""

    yield

    assert mock_create_release.mock_calls == []
    assert mock_update_release.mock_calls == []
    assert len(responses.calls) == 0


def test_default_dry_run(cli_invoke, scenario1, no_actions, caplog):
    cli_invoke(["github-release", "--dry-run"])
    assert caplog.record_tuples == [
        (
            "scriv.changelog",
            logging.INFO,
            "Reading changelog CHANGELOG.rst",
        ),
        ("scriv.ghrel", logging.INFO, "Would create release v1.2.3"),
        ("scriv.ghrel", logging.INFO, "Body:\nA good release\n"),
    ]


def test_dash_all_dry_run(cli_invoke, scenario1, no_actions, caplog):
    cli_invoke(["github-release", "--all", "--dry-run"])
    assert caplog.record_tuples == [
        (
            "scriv.changelog",
            logging.INFO,
            "Reading changelog CHANGELOG.rst",
        ),
        ("scriv.ghrel", logging.INFO, "Would create release v1.2.3"),
        ("scriv.ghrel", logging.INFO, "Body:\nA good release\n"),
        (
            "scriv.ghrel",
            logging.WARNING,
            "Entry 'Some fixes' has no version, skipping.",
        ),
        ("scriv.ghrel", logging.INFO, "Would update release v0.9a7"),
        ("scriv.ghrel", logging.INFO, "Body:\nA beginning\n"),
        (
            "scriv.ghrel",
            logging.WARNING,
            "Version v0.1 has no tag. No release will be made.",
        ),
        ("scriv.ghrel", 20, "Would update release v0.0.1"),
        ("scriv.ghrel", 20, "Body:\nVery first.\n"),
    ]


def test_no_github_repo(cli_invoke, scenario1, fake_git):
    fake_git.remove_remote("origin")
    result = cli_invoke(["github-release"], expect_ok=False)
    assert result.exit_code == 1
    assert result.output == (
        "Reading changelog CHANGELOG.rst\n" + "Couldn't find a GitHub repo\n"
    )


def test_no_clear_github_repo(cli_invoke, scenario1, fake_git):
    # Add another GitHub remote, now there are two.
    fake_git.add_remote("upstream", "git@github.com:psf/project.git")
    result = cli_invoke(["github-release"], expect_ok=False)
    assert result.exit_code == 1
    assert result.output == (
        "Reading changelog CHANGELOG.rst\n"
        + "More than one GitHub repo found: joe/project, psf/project\n"
    )


def test_with_template(cli_invoke, temp_dir, scenario1, mock_create_release):
    (temp_dir / "setup.cfg").write_text(
        """
        [scriv]
        ghrel_template = |{{body}}|{{config.format}}|{{version}}
        """
    )
    cli_invoke(["github-release"])

    expected = dict(V123_REL)
    expected["body"] = "|A good release\n|rst|v1.2.3"

    assert mock_create_release.mock_calls == [call("joe/project", expected)]
scriv-1.4.0/tests/test_github.py000066400000000000000000000100161451174641100167010ustar00rootroot00000000000000"""Tests of scriv/github.py"""

import json
import logging

import pytest
import requests

from scriv.github import (
    create_release,
    get_releases,
    github_paginated,
    update_release,
)


def test_one_page(responses):
    url = "https://api.github.com/repos/user/small_project/tags"
    data = [{"tag": word} for word in ["one", "two", "three", "four"]]
    responses.add(responses.GET, url, json=data)
    res = list(github_paginated(url))
    assert res == data


def test_three_pages(responses):
    # Three pages, referring to each other in the "link" header.
    url = "https://api.github.com/repos/user/large_project/tags"
    next_url = "https://api.github.com/repositories/138421996/tags"
    next_urls = [f"{next_url}?page={num}" for num in range(1, 4)]
    data = [
        [{"tag": f"{word}{num}"} for word in ["one", "two", "three", "four"]]
        for num in range(3)
    ]
    responses.add(
        responses.GET,
        url,
        json=data[0],
        headers={
            "link": f'<{next_urls[1]}>; rel="next", '
            + f'<{next_urls[2]}>; rel="last", ',
        },
    )
    responses.add(
        responses.GET,
        next_urls[1],
        json=data[1],
        headers={
            "link": f'<{next_urls[0]}>; rel="prev", '
            + f'<{next_urls[2]}>; rel="next", '
            + f'<{next_urls[2]}>; rel="last", '
            + f'<{next_urls[0]}>; rel="first"',
        },
    )
    responses.add(
        responses.GET,
        next_urls[2],
        json=data[2],
        headers={
            "link": f'<{next_urls[1]}>; rel="prev", '
            + f'<{next_urls[0]}>; rel="first"',
        },
    )
    res = list(github_paginated(url))
    assert res == data[0] + data[1] + data[2]


def test_bad_page(responses):
    url = "https://api.github.com/repos/user/small_project/secretstuff"
    responses.add(responses.GET, url, json=[], status=403)
    with pytest.raises(requests.HTTPError, match="403 Client Error"):
        list(github_paginated(url))


def test_get_releases(responses):
    url = "https://api.github.com/repos/user/small/releases"
    responses.add(
        responses.GET,
        url,
        json=[
            {"tag_name": "a", "name": "a", "prerelease": False},
            {"tag_name": "b", "name": "b", "prerelease": True},
        ],
    )
    releases = get_releases("user/small")
    assert releases == {
        "a": {"tag_name": "a", "name": "a", "prerelease": False},
        "b": {"tag_name": "b", "name": "b", "prerelease": True},
    }


RELEASE_DATA = {
    "name": "v3.14",
    "tag_name": "v3.14",
    "draft": False,
    "prerelease": False,
    "body": "this is a great release",
}


def test_create_release(responses, caplog):
    responses.add(
        responses.POST,
        "https://api.github.com/repos/someone/something/releases",
    )
    create_release("someone/something", RELEASE_DATA)
    assert json.loads(responses.calls[0].request.body) == RELEASE_DATA
    assert caplog.record_tuples == [
        ("scriv.github", logging.INFO, "Creating release v3.14")
    ]


def test_create_release_fails(responses):
    responses.add(
        responses.POST,
        "https://api.github.com/repos/someone/something/releases",
        status=500,
    )
    with pytest.raises(requests.HTTPError, match="500 Server Error"):
        create_release("someone/something", RELEASE_DATA)


def test_update_release(responses, caplog):
    url = "https://api.github.com/repos/someone/something/releases/60006815"
    responses.add(responses.PATCH, url)
    release = {"url": url}
    update_release(release, RELEASE_DATA)
    assert json.loads(responses.calls[0].request.body) == RELEASE_DATA
    assert caplog.record_tuples == [
        ("scriv.github", logging.INFO, "Updating release v3.14")
    ]


def test_update_release_fails(responses):
    url = "https://api.github.com/repos/someone/something/releases/60006815"
    responses.add(responses.PATCH, url, status=500)
    release = {"url": url}
    with pytest.raises(requests.HTTPError, match="500 Server Error"):
        update_release(release, RELEASE_DATA)
scriv-1.4.0/tests/test_gitinfo.py000066400000000000000000000051021451174641100170560ustar00rootroot00000000000000"""Tests of gitinfo.py"""

import re

from scriv.gitinfo import current_branch_name, get_github_repos, user_nick


def test_user_nick_from_scriv_user_nick(fake_git):
    fake_git.set_config("scriv.user_nick", "joedev")
    assert user_nick() == "joedev"


def test_user_nick_from_github(fake_git):
    fake_git.set_config("github.user", "joedev")
    assert user_nick() == "joedev"


def test_user_nick_from_git(fake_git):
    fake_git.set_config("user.email", "joesomeone@somewhere.org")
    assert user_nick() == "joesomeone"


def test_user_nick_from_env(fake_git, monkeypatch):
    monkeypatch.setenv("USER", "joseph")
    assert user_nick() == "joseph"


def test_user_nick_from_nowhere(fake_git, monkeypatch):
    # With no git information, and no USER env var,
    # we just call the user "somebody"
    monkeypatch.delenv("USER", raising=False)
    assert user_nick() == "somebody"


def test_current_branch_name(fake_git):
    fake_git.set_branch("joedev/feature-123")
    assert current_branch_name() == "joedev/feature-123"


def test_get_github_repos_no_remotes(fake_git):
    assert get_github_repos() == set()


def test_get_github_repos_one_github_remote(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject.git")
    assert get_github_repos() == {"joe/myproject"}


def test_get_github_repos_one_github_remote_no_extension(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject")
    assert get_github_repos() == {"joe/myproject"}


def test_get_github_repos_two_github_remotes(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject.git")
    fake_git.add_remote("upstream", "git@github.com:psf/myproject.git")
    assert get_github_repos() == {"joe/myproject", "psf/myproject"}


def test_get_github_repos_one_github_plus_others(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject.git")
    fake_git.add_remote("upstream", "git@gitlab.com:psf/myproject.git")
    assert get_github_repos() == {"joe/myproject"}


def test_get_github_repos_no_github_remotes(fake_git):
    fake_git.add_remote("mygitlab", "git@gitlab.com:joe/myproject.git")
    fake_git.add_remote("upstream", "git@gitlab.com:psf/myproject.git")
    assert get_github_repos() == set()


def test_real_get_github_repos():
    # Since we don't know the name of this repo (forks could be anything),
    # we can't be sure what we get, except it should be word/word, and not end
    # with .git
    repos = get_github_repos()
    assert len(repos) >= 1
    repo = repos.pop()
    assert re.fullmatch(r"\w+/\w+", repo)
    assert not repo.endswith(".git")
scriv-1.4.0/tests/test_literals.py000066400000000000000000000125501451174641100172430ustar00rootroot00000000000000"""Tests of literals.py"""

import os
import sys

import pytest

import scriv.literals
from scriv.exceptions import ScrivException
from scriv.literals import find_literal
from scriv.optional import tomllib, yaml


def test_no_extras_craziness():
    # Check that if we're testing no-extras we didn't get the modules, and if we
    # aren't, then we did get the modules.
    if os.getenv("SCRIV_TEST_NO_EXTRAS", ""):
        if sys.version_info < (3, 11):
            assert tomllib is None
        assert yaml is None
    else:
        assert tomllib is not None
        assert yaml is not None


PYTHON_CODE = """\
# A string we should get.
version = "1.2.3"

typed_version: Final[str] = "2.3.4"

thing1.attr = "3.4.5"
thing2.attr: str = "4.5.6"

# Numbers don't count.
how_many = 123

# Complex names don't count.
a_thing[0] = 123

# Non-constant values don't count.
a_thing_2 = func(1)

# Non-strings don't count.
version = compute_version(1)

if 1:
    # It's OK if they are inside other structures.
    also = "xyzzy"
    but = '''hello there'''

def foo():
    # Even in a function is OK, but why would you do that?
    somewhere_else = "this would be an odd place to get the string"
"""


@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1.2.3"),
        ("typed_version", "2.3.4"),
        ("also", "xyzzy"),
        ("but", "hello there"),
        ("somewhere_else", "this would be an odd place to get the string"),
        ("a_thing_2", None),
        ("how_many", None),
    ],
)
def test_find_python_literal(name, value, temp_dir):
    with open("foo.py", "w", encoding="utf-8") as f:
        f.write(PYTHON_CODE)
    assert find_literal("foo.py", name) == value


def test_unknown_file_type(temp_dir):
    with open("what.xyz", "w", encoding="utf-8") as f:
        f.write("Hello there!")
    expected = "Can't read literals from files like 'what.xyz'"
    with pytest.raises(ScrivException, match=expected):
        find_literal("what.xyz", "hi")


TOML_LITERAL = """
version = "1"

[tool.poetry]
version = "2"

[metadata]
version = "3"
objects = { version = "4", other = "ignore" }

[bogus]
# Non-strings don't count.
number = 123
boolean = true
lists = [1, 2, 3]
bad_type = nan

# Sections don't count.
[bogus.section]

"""


@pytest.mark.skipif(tomllib is None, reason="No TOML support installed")
@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1"),
        ("tool.poetry.version", "2"),
        ("tool.poetry.version.too.deep", None),
        ("metadata.version", "3"),
        ("metadata.objects.version", "4"),
        ("bogus", None),
        ("bogus.number", None),
        ("bogus.boolean", None),
        ("bogus.lists", None),
        ("bogus.bad_type", None),
        ("bogus.section", None),
        ("bogus.section.too.deep", None),
    ],
)
def test_find_toml_literal(name, value, temp_dir):
    with open("foo.toml", "w", encoding="utf-8") as f:
        f.write(TOML_LITERAL)
    assert find_literal("foo.toml", name) == value


def test_find_toml_literal_fail_if_unavailable(monkeypatch):
    monkeypatch.setattr(scriv.literals, "tomllib", None)
    with pytest.raises(
        ScrivException, match="Can't read .+ without TOML support"
    ):
        find_literal("foo.toml", "fail")


YAML_LITERAL = """\
---
version: 1.2.3

myVersion:
  MAJOR: 2
  MINOR: 3
  PATCH: 5

myproduct:
  version: [mayor=5, minor=6, patch=7]
  versionString: "8.9.22"
...
"""


@pytest.mark.skipif(yaml is None, reason="No YAML support installed")
@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1.2.3"),
        ("myproduct.versionString", "8.9.22"),
        ("myproduct.version", None),
        ("myVersion", None),
    ],
)
def test_find_yaml_literal(name, value, temp_dir):
    with open("foo.yml", "w", encoding="utf-8") as f:
        f.write(YAML_LITERAL)
    assert find_literal("foo.yml", name) == value


def test_find_yaml_literal_fail_if_unavailable(monkeypatch):
    monkeypatch.setattr(scriv.literals, "yaml", None)
    with pytest.raises(
        ScrivException, match="Can't read .+ without YAML support"
    ):
        find_literal("foo.yml", "fail")


CFG_LITERAL = """\

[metadata]
name = myproduct
version = 1.2.3
url = https://github.com/nedbat/scriv
description = A nice description
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT

[options]
zip_safe = false
include_package_data = true

[bdist_wheel]
universal = true

[coverage:report]
show_missing = true

[flake8]
max-line-length = 99
doctests = True
exclude =  .git, .eggs, __pycache__, tests/, docs/, build/, dist/
"""


@pytest.mark.parametrize(
    "name, value",
    [
        ("metadata.version", "1.2.3"),
        ("options.zip_safe", "false"),
        ("coverage:report", None),  # find_literal only supports string values
        ("metadata.myVersion", None),
        ("unexisting", None),
    ],
)
def test_find_cfg_literal(name, value, temp_dir):
    with open("foo.cfg", "w", encoding="utf-8") as f:
        f.write(CFG_LITERAL)
    assert find_literal("foo.cfg", name) == value


CABAL_LITERAL = """\
cabal-version:      3.0
name:               pkg
version:            1.2.3
"""


@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1.2.3"),
    ],
)
def test_find_cabal_literal(name, value, temp_dir):
    with open("foo.cabal", "w", encoding="utf-8") as f:
        f.write(CABAL_LITERAL)
    assert find_literal("foo.cabal", name) == value
scriv-1.4.0/tests/test_process.py000066400000000000000000000005351451174641100171020ustar00rootroot00000000000000"""Tests of the process behavior of scriv."""

import sys

from scriv import __version__
from scriv.shell import run_command


def test_dashm():
    ok, output = run_command([sys.executable, "-m", "scriv"])
    print(output)
    assert ok
    assert "Usage: scriv [OPTIONS] COMMAND [ARGS]..." in output
    assert "Version " + __version__ in output
scriv-1.4.0/tests/test_util.py000066400000000000000000000036671451174641100164120ustar00rootroot00000000000000"""Tests of scriv/util.py"""

import pytest

from scriv.util import Version, partition_lines


@pytest.mark.parametrize(
    "text, ver",
    [
        ("v1.2.3 -- 2022-04-06", "v1.2.3"),
        ("Oops, fixed on 6/16/2021.", None),
        ("2022-Apr-06: 12.3-alpha0 finally", "12.3-alpha0"),
        ("2.7.19beta1, 2022-04-08", "2.7.19beta1"),
    ],
)
def test_version_from_text(text, ver):
    if ver is not None:
        ver = Version(ver)
    assert Version.from_text(text) == ver


@pytest.mark.parametrize(
    "version",
    [
        "v1.2.3",
        "17.4.1.3",
    ],
)
def test_is_not_prerelease_version(version):
    assert not Version(version).is_prerelease()


@pytest.mark.parametrize(
    "version",
    [
        "v1.2.3a1",
        "17.4.1.3-beta.2",
    ],
)
def test_is_prerelease_version(version):
    assert Version(version).is_prerelease()


VERSION_EQUALITIES = [
    ("v1.2.3a1", "v1.2.3a1", True),
    ("1.2.3a1", "v1.2.3a1", True),
    ("v1.2.3a1", "1.2.3a1", True),
    ("1.2.3a1", "1.2.3a1", True),
    ("1.2", "1.2.0", False),
    ("1.2.3", "1.2.3a1", False),
    ("1.2.3a1", "1.2.3b1", False),
    ("v1.2.3", "1.2.3a1", False),
]


@pytest.mark.parametrize("ver1, ver2, equal", VERSION_EQUALITIES)
def test_version_equality(ver1, ver2, equal):
    assert (Version(ver1) == Version(ver2)) is equal


@pytest.mark.parametrize("ver1, ver2, equal", VERSION_EQUALITIES)
def test_version_hashing(ver1, ver2, equal):
    assert len({Version(ver1), Version(ver2)}) == (1 if equal else 2)


@pytest.mark.parametrize(
    "text, result",
    [
        ("one\ntwo\nthree\n", ("one\ntwo\nthree\n", "", "")),
        ("oXe\ntwo\nthree\n", ("", "oXe\n", "two\nthree\n")),
        ("one\ntXo\nthree\n", ("one\n", "tXo\n", "three\n")),
        ("one\ntwo\ntXree\n", ("one\ntwo\n", "tXree\n", "")),
        ("one\ntXo\ntXree\n", ("one\n", "tXo\n", "tXree\n")),
    ],
)
def test_partition_lines(text, result):
    assert partition_lines(text, "X") == result
scriv-1.4.0/tox.ini000066400000000000000000000031061451174641100141610ustar00rootroot00000000000000[tox]
envlist =
    # Run on all our Pythons:
    py3{7,8,9,10,11,12},
    # Run with no extras on lowest and highest version:
    py3{7,12}-no_extras,
    # And the rest:
    pypy3, coverage, docs, quality
labels =
    ci-tests = py3{7,8,9,10,11,12},pypy3

[testenv]
package = wheel
wheel_build_env = .pkg
deps =
    -r{toxinidir}/requirements/test.txt
extras =
    !no_extras: toml,yaml
allowlist_externals =
    make
    rm
passenv =
    COVERAGE_*
setenv =
    no_extras: SCRIV_TEST_NO_EXTRAS=1
commands =
    python -V
    no_extras: python -m pip uninstall -q -y tomli
    coverage run -p -m pytest -Wd {posargs}

[testenv:coverage]
depends = py37,py38,py39,py310,py311,py312,pypy3
basepython = python3.12
commands =
    coverage combine -q
    coverage report -m --skip-covered
    coverage html
    coverage json
parallel_show_output = true

[testenv:docs]
setenv =
    PYTHONPATH = {toxinidir}
deps =
    -r{toxinidir}/requirements/doc.txt
commands =
    make -C docs clean html
    doc8 -q --ignore-path docs/include README.rst docs

[testenv:quality]
deps =
    -r{toxinidir}/requirements/quality.txt
commands =
    black --check --diff --line-length=80 src/scriv tests docs setup.py
    python -m cogapp -cP --check --verbosity=1 docs/*.rst
    mypy src/scriv tests
    pylint src/scriv tests docs setup.py
    pycodestyle src/scriv tests docs setup.py
    pydocstyle src/scriv tests docs setup.py
    isort --check-only --diff -p scriv tests src/scriv setup.py
    python setup.py -q sdist
    twine check dist/*

[testenv:upgrade]
commands =
    python -m pip install -U pip
    make upgrade