diff --git a/.copier-answers.yml b/.copier-answers.yml
new file mode 100644
index 0000000..7fdc701
--- /dev/null
+++ b/.copier-answers.yml
@@ -0,0 +1,10 @@
+# Changes here will be overwritten by Copier
+_commit: a84eb20
+_src_path: ../../templates/base
+add_extension: python
+email: t.paine154@gmail.com
+github: python-project-templates
+project_description: Easily generate sphinx documentation
+project_name: yardang
+python_version_primary: '3.11'
+team: the yardang authors
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..7c39058
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,9 @@
+examples/* linguist-documentation
+docs/* linguist-documentation
+*.ipynb linguist-documentation
+Makefile linguist-documentation
+
+*.md text=auto eol=lf
+*.py text=auto eol=lf
+*.toml text=auto eol=lf
+*.yml text=auto eol=lf
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..1b32efd
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at t.paine154@gmail.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..dd84ea7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..42cac77
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,16 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ labels:
+ - "part: github_actions"
+
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ labels:
+ - "lang: python"
+ - "part: dependencies"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..34718d9
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,84 @@
+
+name: Build Status
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - v*
+ paths-ignore:
+ - LICENSE
+ - README.md
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ checks: write
+ pull-requests: write
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ python-version: ["3.11"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+ cache-dependency-path: 'pyproject.toml'
+
+ - name: Install dependencies
+ run: make develop
+
+ - name: Lint
+ run: make lint
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+
+ - name: Checks
+ run: make checks
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+
+ - name: Build
+ run: make build
+
+ - name: Test
+ run: make coverage
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+
+ - name: Upload test results (Python)
+ uses: actions/upload-artifact@v4
+ with:
+ name: pytest-results-${{ matrix.os }}-${{ matrix.python-version }}
+ path: junit.xml
+ if: ${{ always() }}
+
+ - name: Publish Unit Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ with:
+ files: |
+ **/junit.xml
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+
+ - name: Upload coverage
+ uses: codecov/codecov-action@v4
+
+ - name: Make dist
+ run: make dist
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..6ca647c
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,30 @@
+name: Docs
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - v*
+ paths-ignore:
+ - LICENSE
+ - README.md
+
+permissions:
+ contents: write
+
+jobs:
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v4
+ - run: pip install .
+ - run: make docs
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ publish_branch: gh-pages
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: docs/html
+ force_orphan: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1be413e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,144 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+*.dll
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+junit.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# PyBuilder
+target/
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# Documentation
+docs/_build/
+/site
+docs/api
+docs/index.md
+docs/html
+index.md
+_template/labextension
+
+# Jupyter
+.ipynb_checkpoints
+.autoversion
+
+# Mac
+.DS_Store
+
+# Rust
+target
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3e8cd66
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ 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
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2024 the yardang authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..afbf10a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,125 @@
+#########
+# BUILD #
+#########
+.PHONY: develop build install
+
+develop: ## install dependencies and build library
+ python -m pip install -e .[develop]
+
+build: ## build the python library
+ python setup.py build build_ext --inplace
+
+install: ## install library
+ python -m pip install .
+
+#########
+# LINTS #
+#########
+.PHONY: lint lints fix format
+
+lint: ## run python linter with ruff
+ python -m ruff check yardang
+ python -m ruff format --check yardang
+
+# Alias
+lints: lint
+
+fix: ## fix python formatting with ruff
+ python -m ruff check --fix yardang
+ python -m ruff format yardang
+
+# alias
+format: fix
+
+################
+# Other Checks #
+################
+.PHONY: check-manifest checks check annotate
+
+check-manifest: ## check python sdist manifest with check-manifest
+ check-manifest -v
+
+checks: check-manifest
+
+# Alias
+check: checks
+
+annotate: ## run python type annotation checks with mypy
+ python -m mypy ./yardang
+
+#########
+# TESTS #
+#########
+.PHONY: test coverage tests
+
+test: ## run python tests
+ python -m pytest -v yardang/tests --junitxml=junit.xml
+
+coverage: ## run tests and collect test coverage
+ python -m pytest -v yardang/tests --junitxml=junit.xml --cov=yardang --cov-branch --cov-fail-under=1 --cov-report term-missing --cov-report xml
+
+# Alias
+tests: test
+
+########
+# DOCS #
+########
+.PHONY: docs
+
+docs: ## build the documentation
+ yardang build
+
+###########
+# VERSION #
+###########
+.PHONY: show-version patch minor major
+
+show-version: ## show current library version
+ @bump-my-version show current_version
+
+patch: ## bump a patch version
+ @bump-my-version bump patch
+
+minor: ## bump a minor version
+ @bump-my-version bump minor
+
+major: ## bump a major version
+ @bump-my-version bump major
+
+########
+# DIST #
+########
+.PHONY: dist dist-build dist-sdist dist-local-wheel publish
+
+dist-build: # build python dists
+ python -m build -w -s
+
+dist-check: ## run python dist checker with twine
+ python -m twine check dist/*
+
+dist: clean build dist-build dist-check ## build all dists
+
+publish: dist # publish python assets
+
+#########
+# CLEAN #
+#########
+.PHONY: deep-clean clean
+
+deep-clean: ## clean everything from the repository
+ git clean -fdx
+
+clean: ## clean the repository
+ rm -rf .coverage coverage cover htmlcov logs build dist *.egg-info
+
+############################################################################################
+
+.PHONY: help
+
+# Thanks to Francoise at marmelab.com for this
+.DEFAULT_GOAL := help
+help:
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+print-%:
+ @echo '$*=$($*)'
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..925022b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+
+
+# yardang
+
+[![Build Status](https://github.com/python-project-templates/yardang/actions/workflows/build.yml/badge.svg?branch=main&event=push)](https://github.com/python-project-templates/yardang/actions/workflows/build.yml)
+[![codecov](https://codecov.io/gh/python-project-templates/yardang/branch/main/graph/badge.svg)](https://codecov.io/gh/python-project-templates/yardang)
+[![License](https://img.shields.io/github/license/python-project-templates/yardang)](https://github.com/python-project-templates/yardang)
+[![PyPI](https://img.shields.io/pypi/v/yardang.svg)](https://pypi.python.org/pypi/yardang)
+
+`yardang` is a Python library for generating [Sphinx documentation](https://www.sphinx-doc.org/en/master/) easily, with minimal local configuration overhead.
+
+[`yardang`](https://www.britannica.com/science/yardang) makes building [Sphinx](https://www.sphinx-doc.org/en/master/) easy.
+
+*This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base).*
diff --git a/docs/CNAME b/docs/CNAME
new file mode 100644
index 0000000..50f155b
--- /dev/null
+++ b/docs/CNAME
@@ -0,0 +1 @@
+yardang.python-templates.dev
\ No newline at end of file
diff --git a/docs/logo.png b/docs/logo.png
new file mode 100644
index 0000000..c850ade
Binary files /dev/null and b/docs/logo.png differ
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b7459ec
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,140 @@
+[build-system]
+requires = ["hatchling"]
+build-backend="hatchling.build"
+
+[project]
+name = "yardang"
+authors = [{name = "the yardang authors", email = "t.paine154@gmail.com"}]
+description="Easily generate sphinx documentation"
+readme = "README.md"
+license = { text = "Apache-2.0" }
+version = "0.1.0"
+requires-python = ">=3.9"
+keywords = []
+
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Programming Language :: Python :: 3",
+ "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",
+]
+
+dependencies = [
+ "autodoc-pydantic",
+ "furo",
+ "myst-parser",
+ "packaging",
+ "rich",
+ "ruff",
+ "sphinx==7.2.6",
+ "sphinx-autoapi",
+ "sphinx-copybutton",
+ "sphinx-design",
+ "toml",
+ "typer",
+]
+
+[project.urls]
+Repository = "https://github.com/python-project-templates/yardang"
+Homepage = "https://github.com/python-project-templates/yardang"
+
+[project.optional-dependencies]
+develop = [
+ "build",
+ "bump-my-version",
+ "check-manifest",
+ "hatchling",
+ "pytest",
+ "pytest-cov",
+ "ruff",
+ "twine",
+ "wheel",
+]
+
+[project.scripts]
+yardang = "yardang.cli:main"
+
+[tool.bumpversion]
+current_version = "0.1.0"
+commit = true
+tag = true
+
+[[tool.bumpversion.files]]
+filename = "yardang/__init__.py"
+search = '__version__ = "{current_version}"'
+replace = '__version__ = "{new_version}"'
+
+[[tool.bumpversion.files]]
+filename = "pyproject.toml"
+search = 'version = "{current_version}"'
+replace = 'version = "{new_version}"'
+
+[tool.check-manifest]
+ignore = [
+ ".copier-answers.yml",
+ "docs/*",
+ "Makefile",
+ "setup.py",
+]
+
+[tool.hatch.build]
+artifacts = []
+
+[tool.hatch.build.sources]
+src = "/"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/yardang",
+ "LICENSE",
+ "README.md",
+]
+exclude = [
+ "/.github",
+ "/.gitattributes",
+ "/.gitignore",
+ "/docs",
+]
+
+[tool.hatch.build.targets.wheel]
+include = [
+ "/yardang",
+]
+exclude = [
+ "/.github",
+ "/.gitattributes",
+ "/.gitignore",
+ "/docs",
+ "/pyproject.toml",
+]
+
+[tool.hatch.build.targets.wheel.shared-data]
+
+[tool.pytest.ini_options]
+asyncio_mode = "strict"
+testpaths = "yardang/tests"
+
+[tool.ruff]
+line-length = 150
+
+[tool.ruff.lint.isort]
+combine-as-imports = true
+default-section = "third-party"
+known-first-party = ["yardang"]
+section-order = [
+ "future",
+ "third-party",
+ "first-party",
+ "local-folder",
+]
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["F401"]
+
+[tool.yardang]
+cname = "yardang.python-templates.dev"
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..aefdf20
--- /dev/null
+++ b/setup.py
@@ -0,0 +1 @@
+__import__("setuptools").setup()
diff --git a/yardang/__init__.py b/yardang/__init__.py
new file mode 100644
index 0000000..3dc1f76
--- /dev/null
+++ b/yardang/__init__.py
@@ -0,0 +1 @@
+__version__ = "0.1.0"
diff --git a/yardang/build.py b/yardang/build.py
new file mode 100644
index 0000000..95d80a3
--- /dev/null
+++ b/yardang/build.py
@@ -0,0 +1,133 @@
+import os.path
+from contextlib import contextmanager
+from jinja2 import Environment, FileSystemLoader
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import List, Optional
+from .utils import get_config
+
+__all__ = (
+ "generate_docs_configuration",
+ "CUSTOM_CSS",
+)
+
+# Wider screen for furo
+CUSTOM_CSS = """
+/* Wide main page */
+.content {
+ flex: 1;
+}
+aside.sidebar-drawer {
+ width: unset;
+}
+
+/* Left-align tables */
+article table.align-default {
+ margin-left: 0;
+}
+"""
+
+
+@contextmanager
+def generate_docs_configuration(
+ *,
+ project: str = "",
+ title: str = "",
+ module: str = "",
+ description: str = "",
+ author: str = "",
+ version: str = "",
+ theme: str = "furo",
+ docs_root: str = "",
+ cname: str = "",
+ pages: Optional[List] = None,
+ use_autoapi: Optional[bool] = None,
+):
+ if os.path.exists("conf.py"):
+ # yield folder path to sphinx build
+ yield os.path.curdir
+ else:
+ # load configuration
+ default_data = os.path.split(os.getcwd())[-1]
+ project = project or get_config(section="name", base="project") or default_data.replace("_", "-")
+ title = title or get_config(section="title") or default_data.replace("_", "-")
+ module = module or project.replace("-", "_") or default_data.replace("-", "_")
+ description = description or get_config(section="name", base="description") or default_data.replace("_", " ").replace("-", " ")
+ author = author or get_config(section="authors", base="project")
+ if isinstance(author, list) and len(author) > 0:
+ author = author[0]
+ else:
+ author = f"The {project} authors"
+ theme = theme or get_config(section="theme")
+ version = version or get_config(section="version", base="project")
+ docs_root = (
+ docs_root
+ or get_config(section="docs-host")
+ or get_config(section="urls.Homepage", base="project")
+ or get_config(section="urls.homepage", base="project")
+ or get_config(section="urls.Documentation", base="project")
+ or get_config(section="urls.documentation", base="project")
+ or get_config(section="urls.Source", base="project")
+ or get_config(section="urls.source", base="project")
+ or ""
+ )
+ cname = cname or get_config(section="cname")
+ pages = pages or get_config(section="pages") or []
+ use_autoapi = use_autoapi or get_config(section="use-autoapi")
+ source_dir = os.path.curdir
+ autodoc_pydantic_args = {}
+ for f in (
+ "autodoc_pydantic_model_show_config_summary",
+ "autodoc_pydantic_model_show_validator_summary",
+ "autodoc_pydantic_model_show_validator_members",
+ "autodoc_pydantic_field_list_validators",
+ "autodoc_pydantic_field_show_constraints",
+ "autodoc_pydantic_model_member_order",
+ "autodoc_pydantic_model_show_json",
+ "autodoc_pydantic_settings_show_json",
+ "autodoc_pydantic_model_show_field_summary",
+ ):
+ default_value = {"autodoc_pydantic_model_member_order": '"bysource"', "autodoc_pydantic_model_show_json": True}.get(f, False)
+ config_value = get_config(section=f"{f}")
+ autodoc_pydantic_args[f] = default_value if config_value is None else config_value
+ # create a temporary directory to store the conf.py file in
+ with TemporaryDirectory() as td:
+ templateEnv = Environment(loader=FileSystemLoader(searchpath=str(Path(__file__).parent.resolve())))
+ # load the templatized conf.py file
+ template = templateEnv.get_template("conf.py.j2").render(
+ project=project,
+ title=title,
+ module=module,
+ description=description,
+ author=author,
+ version=version,
+ theme=theme,
+ docs_root=docs_root,
+ cname=cname,
+ pages=pages,
+ use_autoapi=use_autoapi,
+ source_dir=source_dir,
+ **autodoc_pydantic_args,
+ )
+ # dump to file
+ template_file = Path(td) / "conf.py"
+ template_file.write_text(template)
+
+ # append docs-specific ignores to gitignore
+ if Path(".gitignore").exists():
+ has_html_build_folder = False
+ has_index_md = False
+ with open(".gitignore", "r+") as fp:
+ for line in fp:
+ if "docs/html" in line:
+ has_html_build_folder = True
+ if "index.md" in line:
+ has_index_md = True
+ if not has_html_build_folder or not has_index_md:
+ fp.write("\n")
+ if not has_html_build_folder:
+ fp.write("docs/html\n")
+ if not has_index_md:
+ fp.write("index.md\n")
+ # yield folder path to sphinx build
+ yield td
diff --git a/yardang/cli.py b/yardang/cli.py
new file mode 100644
index 0000000..a92a930
--- /dev/null
+++ b/yardang/cli.py
@@ -0,0 +1,52 @@
+from sys import executable
+from pathlib import Path
+from subprocess import Popen, PIPE
+from typer import Typer
+
+from .build import generate_docs_configuration, CUSTOM_CSS
+
+
+def build(quiet: bool = False, debug: bool = False):
+ with generate_docs_configuration() as file:
+ folder = Path("docs/html/_static/styles")
+ css = folder / "custom.css"
+ if not css.exists():
+ folder.mkdir(parents=True, exist_ok=True)
+ css.write_text(CUSTOM_CSS)
+
+ build_cmd = [
+ executable,
+ "-m",
+ "sphinx",
+ ".",
+ "docs/html",
+ "-c",
+ file,
+ ]
+
+ if debug:
+ print(" ".join(build_cmd))
+
+ process = Popen(build_cmd, stdout=PIPE)
+ while process.poll() is None:
+ text = process.stdout.readline().decode("utf-8")
+ if text and not quiet:
+ print(text)
+ text = process.stdout.readline().decode("utf-8")
+ if text and not quiet:
+ print(text)
+
+
+def debug():
+ build(quiet=False, debug=True)
+
+
+def main():
+ app = Typer()
+ app.command("build")(build)
+ app.command("debug")(debug)
+ app()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/yardang/conf.py.j2 b/yardang/conf.py.j2
new file mode 100644
index 0000000..d59fe95
--- /dev/null
+++ b/yardang/conf.py.j2
@@ -0,0 +1,154 @@
+import os
+import os.path
+from packaging.version import Version
+from pathlib import Path
+
+project = "{{project}}"
+module = "{{module}}"
+name = "{{project}}"
+description = """{{description}}"""
+author = """{{author}}"""
+copyright = """{{copyright}}"""
+title = """{{title}}"""
+version = "{{version}}"
+release = "{{version}}"
+html_title = """{{title}} v{{version}}
"""
+docs_host_root = "{{docs_root}}"
+cname = "{{cname}}"
+pages = """
+{% for page in pages %}
+{{ page }}
+{% endfor %}
+"""
+use_autoapi = {{use_autoapi}} # noqa: F821
+
+######################
+# Standardized below #
+######################
+extensions = [
+ "myst_parser",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.napoleon",
+ "sphinx_design",
+ "sphinx_copybutton",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.inheritance_diagram",
+ "sphinxcontrib.autodoc_pydantic",
+]
+if use_autoapi in (True, None):
+ # add if it is set to true or if it is set to None
+ extensions.append("autoapi.extension")
+
+os.environ["SPHINX_BUILDING"] = "1"
+html_theme = "{{theme}}"
+html_theme_options = {}
+html_static_path = []
+html_css_files = [
+ "styles/custom.css",
+]
+master_doc = "index"
+templates_path = ["_templates"]
+source_suffix = [".rst", ".md"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "node_modules", "_skbuild", ".pytest_cache", "js/*"]
+language = "en"
+pygments_style = "sphinx"
+autosummary_generate = True
+autoapi_dirs = [module]
+autoapi_python_class_content = "both"
+myst_enable_extensions = ["colon_fence"]
+autodoc_default_options = {
+ "show-inheritance": True,
+}
+autodoc_pydantic_model_show_config_summary = {{autodoc_pydantic_model_show_config_summary}} # noqa: F821
+autodoc_pydantic_model_show_validator_summary = {{autodoc_pydantic_model_show_validator_summary}} # noqa: F821
+autodoc_pydantic_model_show_validator_members = {{autodoc_pydantic_model_show_validator_members}} # noqa: F821
+autodoc_pydantic_field_list_validators = {{autodoc_pydantic_field_list_validators}} # noqa: F821
+autodoc_pydantic_field_show_constraints = {{autodoc_pydantic_field_show_constraints}} # noqa: F821
+autodoc_pydantic_model_member_order = {{autodoc_pydantic_model_member_order}} # noqa: F821
+autodoc_pydantic_model_show_json = {{autodoc_pydantic_model_show_json}} # noqa: F821
+autodoc_pydantic_settings_show_json = {{autodoc_pydantic_settings_show_json}} # noqa: F821
+autoapi_add_toctree_entry = use_autoapi is True
+toctree_base = """{toctree}
+---
+caption: ""
+maxdepth: 2
+hidden: true
+---"""
+toctree_root = f"""```{toctree_base}
+{pages}
+```
+"""
+
+
+def run_copyreadme(_):
+ out = Path("{{source_dir}}") / "index.md"
+ readme = Path("{{source_dir}}") / "README.md"
+ if "index.md" not in pages:
+ out.write_text(toctree_root)
+ out.write_text(readme.read_text())
+
+def run_copycname(_):
+ out = Path("{{source_dir}}") / "docs" / "html" / "CNAME"
+ if cname:
+ out.write_text(cname)
+
+def run_create_version_marker_to_be_committed(_):
+ versions_folder = Path("{{source_dir}}") / "docs" / "versions"
+ if not versions_folder.exists():
+ versions_folder.mkdir(parents=True, exist_ok=True)
+ version_file = versions_folder / f"{version}.txt"
+ version_file.write_text("commit this file to ensure these docs can be referenced in the future")
+
+def run_create_older_version_docs(_):
+ versions_folder = Path("{{source_dir}}") / "docs" / "versions"
+ if not versions_folder.exists():
+ # no older versions yet
+ return
+ all_versions = [f.replace(".txt", "") for f in os.listdir(str(versions_folder)) if f.endswith(".txt")]
+ all_versions_as_versions = []
+ invalid_version = Version("999.999.999")
+ for version in all_versions:
+ try:
+ all_versions_as_versions.append(Version(version))
+ except BaseException:
+ all_versions_as_versions.append(invalid_version)
+ all_versions_as_versions.sort(reverse=True)
+ out = Path("{{source_dir}}") / "docs" / "versions" / "versions.md"
+ out.write_text("# Previous Versions\n\n")
+ for i, older_version in enumerate(all_versions_as_versions):
+ if older_version != invalid_version and str(older_version) in all_versions:
+ older_version_literal = str(older_version)
+ out.write_text(f"- [{older_version_literal}]({docs_host_root}/{name}/{older_version_literal}/)\n")
+ out.write_text("\n")
+
+def run_add_version_links_to_toctree(app, doctree):
+ from sphinx.addnodes import toctree
+ insert = True
+ if app.env.docname == "index":
+ all_docs = set()
+ nodes = list(doctree.traverse(toctree))
+ toc_entry = "docs/versions/versions"
+ if not nodes:
+ return
+ # Capture all existing toctree entries
+ for node in nodes:
+ for entry in node["entries"]:
+ all_docs.add(entry[1])
+ # Don't insert version links it's already present
+ for doc in all_docs:
+ if doc.find("versions") != -1:
+ insert = False
+ if insert:
+ # Insert index
+ nodes[-1]["entries"].append((None, toc_entry))
+ nodes[-1]["includefiles"].append(toc_entry)
+
+def setup(app):
+ {# app.connect("builder-inited", run_create_older_version_docs) #}
+ {# app.connect("builder-inited", run_create_version_marker_to_be_committed) #}
+ app.connect("builder-inited", run_copyreadme)
+ app.connect("builder-inited", run_copycname)
+ {# app.connect("doctree-read", run_add_version_links_to_toctree, priority=500) #}
diff --git a/yardang/tests/test_all.py b/yardang/tests/test_all.py
new file mode 100644
index 0000000..dab4f73
--- /dev/null
+++ b/yardang/tests/test_all.py
@@ -0,0 +1,5 @@
+from yardang import * # noqa
+
+
+def test_all():
+ assert True
diff --git a/yardang/utils.py b/yardang/utils.py
new file mode 100644
index 0000000..4fb3ced
--- /dev/null
+++ b/yardang/utils.py
@@ -0,0 +1,24 @@
+import os
+import toml
+from pathlib import Path
+
+
+__all__ = ("get_config",)
+
+
+def get_pyproject_toml():
+ cwd = os.getcwd()
+ local_path = Path(cwd) / "pyproject.toml"
+ if local_path.exists():
+ return toml.loads(local_path.read_text())
+ raise FileNotFoundError(str(local_path))
+
+
+def get_config(section="", base="tool.yardang"):
+ config = get_pyproject_toml()
+ sections = base.split(".") + (section.split(".") if section else [])
+ for s in sections:
+ config = config.get(s, None)
+ if config is None:
+ return None
+ return config