From 58557059521ed92f5d850abd880c4277c850112d Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 1 Aug 2024 21:39:04 +0200 Subject: [PATCH 01/85] Move to mkdocs --- .github/workflows/mkdocs.yml | 82 ++++ .gitignore | 4 +- .readthedocs.yaml | 17 - docs/_static/extra.css | 119 ------ docs/_static/extra.js | 13 - docs/api/basic.rst | 5 - docs/api/internal.rst | 14 - docs/api/packets.rst | 45 --- docs/api/protocol.rst | 24 -- docs/api/types/index.rst | 12 - docs/api/types/nbt.rst | 6 - docs/assets/py-mine_logo.png | Bin 0 -> 208374 bytes docs/conf.py | 223 ----------- docs/examples/index.rst | 13 - docs/examples/status.rst | 5 - docs/extensions/attributetable.py | 295 -------------- docs/index.md | 82 ++++ docs/index.rst | 35 -- docs/pages/changelog.rst | 12 - docs/pages/code-of-conduct.rst | 159 -------- docs/pages/contributing.rst | 9 - docs/pages/faq.rst | 21 - docs/pages/installation.rst | 28 -- docs/pages/version_guarantees.rst | 38 -- docs/usage/authentication.rst | 268 ------------- docs/usage/index.rst | 13 - mkdocs.yml | 69 ++++ poetry.lock | 640 ++++++++++++++++-------------- pyproject.toml | 11 +- 29 files changed, 581 insertions(+), 1681 deletions(-) create mode 100644 .github/workflows/mkdocs.yml delete mode 100644 .readthedocs.yaml delete mode 100644 docs/_static/extra.css delete mode 100644 docs/_static/extra.js delete mode 100644 docs/api/basic.rst delete mode 100644 docs/api/internal.rst delete mode 100644 docs/api/packets.rst delete mode 100644 docs/api/protocol.rst delete mode 100644 docs/api/types/index.rst delete mode 100644 docs/api/types/nbt.rst create mode 100644 docs/assets/py-mine_logo.png delete mode 100644 docs/conf.py delete mode 100644 docs/examples/index.rst delete mode 100644 docs/examples/status.rst delete mode 100644 docs/extensions/attributetable.py create mode 100644 docs/index.md delete mode 100644 docs/index.rst delete mode 100644 docs/pages/changelog.rst delete mode 100644 docs/pages/code-of-conduct.rst delete mode 100644 docs/pages/contributing.rst delete mode 100644 docs/pages/faq.rst delete mode 100644 docs/pages/installation.rst delete mode 100644 docs/pages/version_guarantees.rst delete mode 100644 docs/usage/authentication.rst delete mode 100644 docs/usage/index.rst create mode 100644 mkdocs.yml diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml new file mode 100644 index 00000000..4425b36b --- /dev/null +++ b/.github/workflows/mkdocs.yml @@ -0,0 +1,82 @@ +--- +name: MkDocs + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + deploy-docs: + runs-on: ubuntu-latest + steps: + - name: Generate token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: "${{ steps.app-token.outputs.token }}" + + # Make the github application be the committer + # (see: https://stackoverflow.com/a/74071223 on how to obtain the committer email) + - name: Setup git config + run: | + git config --global user.name "py-mine-ci-bot" + git config --global user.email "121461646+py-mine-ci-bot[bot]@users.noreply.github.com" + + - name: Setup poetry + id: poetry_setup + uses: ItsDrike/setup-poetry@v1 + with: + python-version: 3.12 + install-args: "--only main,docs" + + - name: Generate docs directory hash + run: | + docs_dir_hash="$(find "./docs" -type f -exec sha256sum {} + | sort | sha256sum | awk '{print $1}')" + echo "docs_dir_hash=$docs_hash" >> $GITHUB_ENV + + - name: Restore MkDocs cache + uses: actions/cache@v4 + with: + path: .cache + key: + "mkdocs-material-${{ steps.poetry_setup.outputs.python-version }}-\ + ${{ hashFiles('./poetry.lock') }}-${{ hashFiles('./mkdocs.yml') }}-\ + ${{ env.docs_dir_hash }}" + restore-keys: "mkdocs-material-${{ steps.poetry_setup.outputs.python-version }}-" + + - name: Build the documentation + run: poetry run mkdocs build + + - name: Deploy preview + if: ${{ github.event_name == 'pull_request' }} + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: ./site + preview-branch: gh-pages + umbrella-dir: pr-preview + token: ${{ steps.app-token.outputs.token }} + + - name: Deploy production + if: ${{ github.event_name == 'push' }} + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: ./site + clean-exclude: pr-preview/ + token: ${{ steps.app-token.outputs.token }} diff --git a/.gitignore b/.gitignore index 62b5097e..d5a2037f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,8 @@ htmlcov/ .coverage* coverage.xml -# Sphinx documentation -docs/_build/ +# Mkdocs documentation +site/ # Pyenv local version information .python-version diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 8b4835c6..00000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 - -build: - os: ubuntu-22.04 - tools: - python: "3.12" - jobs: - post_create_environment: - - python -m pip install poetry - post_install: - - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --only main,docs,docs-ci - - poetry run poetry-dynamic-versioning - -sphinx: - builder: dirhtml - configuration: "docs/conf.py" - fail_on_warning: true diff --git a/docs/_static/extra.css b/docs/_static/extra.css deleted file mode 100644 index f0373b0c..00000000 --- a/docs/_static/extra.css +++ /dev/null @@ -1,119 +0,0 @@ -html { - word-wrap: anywhere; -} - -body { - --toc-item-spacing-horizontal: 0.5rem; - --admonition-font-size: 0.8em; - - --attribute-table-title: var(--color-content-foreground); - --attribute-table-entry-border: var(--color-foreground-border); - --attribute-table-entry-text: var(--color-api-name); - --attribute-table-entry-hover-border: var(--color-content-foreground); - --attribute-table-entry-hover-background: var(--color-api-background-hover); - --attribute-table-entry-hover-text: var(--color-content-foreground); - --attribute-table-badge: var(--color-api-keyword); -} - -.icon { - user-select: none; -} - -.viewcode-back { - position: absolute; - right: 1em; - background-color: var(--color-code-background); - width: auto; -} - -.toc-drawer { - width: initial; - max-width: 20em; - right: -20em; -} - -.toc-tree ul ul ul ul { - border-left: 1px solid var(--color-background-border); -} - -@media (max-width: 82em) { - body { - font-size: 0.7em; - } - - .toc-tree { - padding-left: 0; - } - - .sidebar-brand-text { - font-size: 1rem; - } - - .sidebar-tree .reference { - padding: 0.5em 1em; - } -} - -/* attribute tables */ -.py-attribute-table { - display: flex; - flex-wrap: wrap; - flex-direction: row; - margin: 0 2em; - padding-top: 16px; -} - -.py-attribute-table-column { - flex: 1 1 auto; -} - -.py-attribute-table-column:not(:first-child) { - margin-top: 1em; -} - -.py-attribute-table-column > span { - color: var(--attribute-table-title); -} - -main .py-attribute-table-column > ul { - list-style: none; - margin: 4px 0px; - padding-left: 0; - font-size: 0.95em; -} - -.py-attribute-table-entry { - margin: 0; - padding: 2px 0; - padding-left: 0.2em; - border-left: 2px solid var(--attribute-table-entry-border); - display: flex; - line-height: 1.2em; -} - -.py-attribute-table-entry > a { - padding-left: 0.5em; - color: var(--attribute-table-entry-text); - flex-grow: 1; -} - -.py-attribute-table-entry > a:hover { - color: var(--attribute-table-entry-hover-text); - text-decoration: none; -} - -.py-attribute-table-entry:hover { - background-color: var(--attribute-table-entry-hover-background); - border-left: 2px solid var(--attribute-table-entry-hover-border); - text-decoration: none; -} - -.py-attribute-table-badge { - flex-basis: 3em; - text-align: right; - font-size: 0.9em; - color: var(--attribute-table-badge); - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; -} diff --git a/docs/_static/extra.js b/docs/_static/extra.js deleted file mode 100644 index 12fd8a08..00000000 --- a/docs/_static/extra.js +++ /dev/null @@ -1,13 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - const tables = document.querySelectorAll( - ".py-attribute-table[data-move-to-id]" - ); - tables.forEach((table) => { - let element = document.getElementById( - table.getAttribute("data-move-to-id") - ); - let parent = element.parentNode; - // insert ourselves after the element - parent.insertBefore(table, element.nextSibling); - }); -}); diff --git a/docs/api/basic.rst b/docs/api/basic.rst deleted file mode 100644 index c68e0204..00000000 --- a/docs/api/basic.rst +++ /dev/null @@ -1,5 +0,0 @@ -Basic Usage -=========== - -.. - TODO: Write this diff --git a/docs/api/internal.rst b/docs/api/internal.rst deleted file mode 100644 index 7a8e0745..00000000 --- a/docs/api/internal.rst +++ /dev/null @@ -1,14 +0,0 @@ -Internal API -============ - -Everything listed on this page is considered internal, and is only present to provide linkable references, and -as an easy quick reference for contributors. These components **are not a part of the public API** and **they -should not be used externally**, as we do not guarantee their backwards compatibility, which means breaking changes -may be introduced between patch versions without any warnings. - -.. automodule:: mcproto.utils.abc - :exclude-members: define - -.. autofunction:: tests.helpers.gen_serializable_test -.. - TODO: Write this diff --git a/docs/api/packets.rst b/docs/api/packets.rst deleted file mode 100644 index 5c2d1a55..00000000 --- a/docs/api/packets.rst +++ /dev/null @@ -1,45 +0,0 @@ -Packets documentation -===================== - -Base classes and interaction functions --------------------------------------- - -.. automodule:: mcproto.packets - :members: - :undoc-members: - :show-inheritance: - - -Handshaking gamestate ---------------------- - -.. automodule:: mcproto.packets.handshaking.handshake - :members: - :undoc-members: - :show-inheritance: - -Status gamestate ----------------- - -.. automodule:: mcproto.packets.status.ping - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: mcproto.packets.status.status - :members: - :undoc-members: - :show-inheritance: - -Login gamestate ---------------- - -.. automodule:: mcproto.packets.login.login - :members: - :undoc-members: - :show-inheritance: - -Play gamestate --------------- - -Not yet implemented diff --git a/docs/api/protocol.rst b/docs/api/protocol.rst deleted file mode 100644 index 92fea8d9..00000000 --- a/docs/api/protocol.rst +++ /dev/null @@ -1,24 +0,0 @@ -Protocol documentation -====================== - -This is the documentation for methods minecraft protocol interactions, connection and buffer. - - -.. attributetable:: mcproto.protocol.base_io.BaseAsyncReader - -.. attributetable:: mcproto.protocol.base_io.BaseSyncReader - -.. automodule:: mcproto.protocol.base_io - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: mcproto.buffer.Buffer - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: mcproto.connection - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/types/index.rst b/docs/api/types/index.rst deleted file mode 100644 index f17972a8..00000000 --- a/docs/api/types/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. api/types documentation master file - -======================= -API Types Documentation -======================= - -Welcome to the API Types documentation! This documentation provides information about the various types used in the API. - -.. toctree:: - :maxdepth: 2 - - nbt.rst diff --git a/docs/api/types/nbt.rst b/docs/api/types/nbt.rst deleted file mode 100644 index e2a4398b..00000000 --- a/docs/api/types/nbt.rst +++ /dev/null @@ -1,6 +0,0 @@ -NBT Format -========== - -.. automodule:: mcproto.types.nbt - :members: - :show-inheritance: diff --git a/docs/assets/py-mine_logo.png b/docs/assets/py-mine_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5e2af94ea3ee876b32b4355bc6b2631f75bb4510 GIT binary patch literal 208374 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

w`oC?Fz3CrWobh+Ka)M}4t15XB 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 (3, 11): - from tomllib import load as toml_parse -else: - from tomli import load as toml_parse - - -# -- Basic project information ----------------------------------------------- - -with Path("../pyproject.toml").open("rb") as f: - pkg_meta: dict[str, str] = toml_parse(f)["tool"]["poetry"] - -project = str(pkg_meta["name"]) -copyright = f"{datetime.datetime.now(tz=datetime.timezone.utc).date().year}, ItsDrike" # noqa: A001 -author = "ItsDrike" - -parsed_version = parse_version(pkg_meta["version"]) -release = str(parsed_version) - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -# Add docs/extensions into python path, allowing custom internal sphinx extensions -# these will now be essentially considered as regualar packages -sys.path.append(str(Path(__file__).parent.joinpath("extensions").absolute())) - -extensions = [ - # official extensions - "sphinx.ext.autodoc", # Automatic documentation generation - "sphinx.ext.autosectionlabel", # Allows referring to sections by their title - "sphinx.ext.extlinks", # Shorten common link patterns - "sphinx.ext.intersphinx", # Used to reference for third party projects: - "sphinx.ext.todo", # Adds todo directive - "sphinx.ext.viewcode", # Links to source files for the documented functions - # external - "sphinxcontrib.towncrier.ext", # Towncrier changelog - "m2r2", # Used to include .md files: - "sphinx_copybutton", # Copyable codeblocks - # internal - "attributetable", # adds attributetable directive, for producing list of methods and attributes of class -] - -# The suffix(es) of source filenames. -source_suffix = [".rst", ".md"] - -# The master toctree document. -master_doc = "index" - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ["_build"] - -# 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" - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "furo" -html_favicon = "https://i.imgur.com/nPCcxts.png" - -html_static_path = ["_static"] -html_css_files = ["extra.css"] -html_js_files = ["extra.js"] - -# -- Extension configuration ------------------------------------------------- - -# -- sphinx.ext.autodoc ------------------------ - -# What docstring to insert into main body of autoclass -# "class" / "init" / "both" -autoclass_content = "both" - -# Sort order of the automatically documented members -autodoc_member_order = "bysource" - -# Default options for all autodoc directives -autodoc_default_options = { - "members": True, - "undoc-members": True, - "show-inheritance": True, - "exclude-members": "__dict__,__weakref__", -} - -# -- sphinx.ext.autosectionlabel --------------- - -# Automatically generate section labels: -autosectionlabel_prefix_document = True - -# -- sphinx.ext.extlinks ----------------------- - -# will create new role, allowing for example :issue:`123` -extlinks = { - # role: (URL with %s, caption or None) - "issue": ("https://github.com/py-mine/mcproto/issues/%s", "GH-%s"), -} - -# -- sphinx.ext.intersphinx -------------------- - -# Third-party projects documentation references: -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), -} - -# -- sphinx.ext.todo --------------------------- - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - -# -- sphinxcontrib.towncrier.ext --------------- - -towncrier_draft_autoversion_mode = "draft" -towncrier_draft_include_empty = True -towncrier_draft_working_directory = Path(__file__).parents[1].resolve() - -# -- m2r2 -------------------------------------- - -# Enable multiple references to the same URL for m2r2 -m2r_anonymous_references = True - -# Changelog contains a lot of duplicate labels, since every subheading holds a category -# and these repeat a lot. Currently, m2r2 doesn't handle this properly, and so these -# labels end up duplicated. See: https://github.com/CrossNox/m2r2/issues/59 -suppress_warnings = [ - "autosectionlabel.pages/changelog", - "autosectionlabel.pages/code-of-conduct", - "autosectionlabel.pages/contributing", -] - -# -- Other options ----------------------------------------------------------- - - -def mock_autodoc() -> None: - """Mock autodoc to not add ``Bases: object`` to the classes, that do not have super classes. - - See also https://stackoverflow.com/a/75041544/20952782. - """ - from sphinx.ext import autodoc - - class MockedClassDocumenter(autodoc.ClassDocumenter): - @override - def add_line(self, line: str, source: str, *lineno: int) -> None: - if line == " Bases: :py:class:`object`": - return - super().add_line(line, source, *lineno) - - autodoc.ClassDocumenter = MockedClassDocumenter - - -def override_towncrier_draft_format() -> None: - """Monkeypatch sphinxcontrib.towncrier.ext to first convert the draft text from md to rst. - - We can use ``m2r2`` for this, as it's an already installed extension with goal - of including markdown documents into rst documents, so we simply run it's converter - somewhere within sphinxcontrib.towncrier.ext and include this conversion. - - Additionally, the current changelog format always starts the version with "Version {}", - this doesn't look well with the version set to "Unreleased changes", so this function - also removes this "Version " prefix. - """ - import m2r2 - import sphinxcontrib.towncrier.ext - from docutils import statemachine - from sphinx.util.nodes import nodes - - orig_f = sphinxcontrib.towncrier.ext._nodes_from_document_markup_source # pyright: ignore[reportPrivateUsage] - - def override_f( - state: statemachine.State, # pyright: ignore[reportMissingTypeArgument] # arg not specified in orig_f either - markup_source: str, - ) -> list[nodes.Node]: - markup_source = markup_source.replace("## Version Unreleased changes", "## Unreleased changes") - markup_source = markup_source.rstrip(" \n") - - # Alternative to 3.9+ str.removesuffix - if markup_source.endswith("---"): - markup_source = markup_source[:-3] - - markup_source = markup_source.rstrip(" \n") - markup_source = m2r2.M2R()(markup_source) - - return orig_f(state, markup_source) - - sphinxcontrib.towncrier.ext._nodes_from_document_markup_source = override_f # pyright: ignore[reportPrivateUsage] - - -mock_autodoc() -override_towncrier_draft_format() diff --git a/docs/examples/index.rst b/docs/examples/index.rst deleted file mode 100644 index 09ef2452..00000000 --- a/docs/examples/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Examples -======== - -Here are some examples of using the project in practice. - - -.. toctree:: - :maxdepth: 1 - :caption: Examples - - status.rst - -Feel free to propose any further examples, we'll be happy to add them to the list! diff --git a/docs/examples/status.rst b/docs/examples/status.rst deleted file mode 100644 index e9c37997..00000000 --- a/docs/examples/status.rst +++ /dev/null @@ -1,5 +0,0 @@ -Obtaining status data from a server -=================================== - -.. - TODO: Write this diff --git a/docs/extensions/attributetable.py b/docs/extensions/attributetable.py deleted file mode 100644 index bbcb8b6e..00000000 --- a/docs/extensions/attributetable.py +++ /dev/null @@ -1,295 +0,0 @@ -from __future__ import annotations - -import importlib -import inspect -import re -from collections.abc import Sequence -from typing import Any, ClassVar, NamedTuple - -from docutils import nodes -from sphinx import addnodes -from sphinx.application import Sphinx -from sphinx.environment import BuildEnvironment -from sphinx.locale import _ as translate -from sphinx.util.docutils import SphinxDirective -from sphinx.util.typing import OptionSpec -from sphinx.writers.html5 import HTML5Translator -from typing_extensions import override - - -class AttributeTable(nodes.General, nodes.Element): - pass - - -class AttributeTableColumn(nodes.General, nodes.Element): - pass - - -class AttributeTableTitle(nodes.TextElement): - pass - - -class AttributeTablePlaceholder(nodes.General, nodes.Element): - pass - - -class AttributeTableBadge(nodes.TextElement): - pass - - -class AttributeTableItem(nodes.Part, nodes.Element): - pass - - -def visit_attributetable_node(self: HTML5Translator, node: AttributeTable) -> None: - class_ = node["python-class"] - self.body.append(f'

") - - -def depart_attributetablecolumn_node(self: HTML5Translator, node: AttributeTableColumn) -> None: - self.body.append("") - - -def depart_attributetabletitle_node(self: HTML5Translator, node: AttributeTableTitle) -> None: - self.body.append("") - - -def depart_attributetablebadge_node(self: HTML5Translator, node: AttributeTableBadge) -> None: - self.body.append("") - - -def depart_attributetable_item_node(self: HTML5Translator, node: AttributeTableItem) -> None: - self.body.append("") - - -_name_parser_regex = re.compile(r"(?P[\w.]+\.)?(?P\w+)") - - -class PyAttributeTable(SphinxDirective): - has_content: ClassVar[bool] = False - required_arguments: ClassVar[int] = 1 - optional_arguments: ClassVar[int] = 0 - final_argument_whitespace: ClassVar[bool] = False - option_spec: ClassVar[OptionSpec | None] = {} - - def parse_name(self, content: str) -> tuple[str, str]: - match = _name_parser_regex.match(content) - if match is None: - raise RuntimeError(f"content {content} somehow doesn't match regex in {self.env.docname}.") - path, name = match.groups() - if path: - modulename = path.rstrip(".") - else: - modulename = self.env.temp_data.get("autodoc:module") - if not modulename: - modulename = self.env.ref_context.get("py:module") - if modulename is None: - raise RuntimeError(f"modulename somehow None for {content} in {self.env.docname}.") - - return modulename, name - - @override - def run(self) -> list[AttributeTablePlaceholder]: - """If you're curious on the HTML this is meant to generate: - - - - However, since this requires the tree to be complete - and parsed, it'll need to be done at a different stage and then - replaced. - """ - content = self.arguments[0].strip() - node = AttributeTablePlaceholder("") - modulename, name = self.parse_name(content) - node["python-doc"] = self.env.docname - node["python-module"] = modulename - node["python-class"] = name - node["python-full-name"] = f"{modulename}.{name}" - return [node] - - -def build_lookup_table(env: BuildEnvironment) -> dict[str, list[str]]: - # Given an environment, load up a lookup table of - # full-class-name: objects - result = {} - domain = env.domains["py"] - - ignored = { - "data", - "exception", - "module", - "class", - } - - for fullname, _, objtype, _, _, _ in domain.get_objects(): - if objtype in ignored: - continue - - classname, _, child = fullname.rpartition(".") - try: - result[classname].append(child) - except KeyError: - result[classname] = [child] - - return result - - -class TableElement(NamedTuple): - fullname: str - label: str - badge: AttributeTableBadge | None - - -def process_attributetable(app: Sphinx, doctree: nodes.Node, fromdocname: str) -> None: - env = app.builder.env - - lookup = build_lookup_table(env) - for node in doctree.traverse(AttributeTablePlaceholder): - modulename, classname, fullname = node["python-module"], node["python-class"], node["python-full-name"] - groups = get_class_results(lookup, modulename, classname, fullname) - table = AttributeTable("") - for label, subitems in groups.items(): - if not subitems: - continue - table.append(class_results_to_node(label, sorted(subitems, key=lambda c: c.label))) - - table["python-class"] = fullname - - if not table: - node.replace_self([]) - else: - node.replace_self([table]) - - -def get_class_results( - lookup: dict[str, list[str]], modulename: str, name: str, fullname: str -) -> dict[str, list[TableElement]]: - module = importlib.import_module(modulename) - cls = getattr(module, name) - - groups: dict[str, list[TableElement]] = { - translate("Attributes"): [], - translate("Methods"): [], - } - - try: - members = lookup[fullname] - except KeyError: - return groups - - for attr in members: - attrlookup = f"{fullname}.{attr}" - key = translate("Attributes") - badge = None - label = attr - value = None - - for base in cls.__mro__: - value = base.__dict__.get(attr) - if value is not None: - break - - if value is not None: - doc = value.__doc__ or "" - if inspect.iscoroutinefunction(value) or doc.startswith("|coro|"): - key = translate("Methods") - badge = AttributeTableBadge("async", "async") - badge["badge-type"] = translate("coroutine") - elif isinstance(value, classmethod): - key = translate("Methods") - label = f"{name}.{attr}" - badge = AttributeTableBadge("cls", "cls") - badge["badge-type"] = translate("classmethod") - elif inspect.isfunction(value): - if doc.startswith(("A decorator", "A shortcut decorator")): - # finicky but surprisingly consistent - key = translate("Methods") - badge = AttributeTableBadge("@", "@") - badge["badge-type"] = translate("decorator") - elif inspect.isasyncgenfunction(value): - key = translate("Methods") - badge = AttributeTableBadge("async for", "async for") - badge["badge-type"] = translate("async iterable") - else: - key = translate("Methods") - badge = AttributeTableBadge("def", "def") - badge["badge-type"] = translate("method") - - groups[key].append(TableElement(fullname=attrlookup, label=label, badge=badge)) - - return groups - - -def class_results_to_node(key: str, elements: Sequence[TableElement]) -> AttributeTableColumn: - title = AttributeTableTitle(key, key) - ul = nodes.bullet_list("") - for element in elements: - ref = nodes.reference( - "", - "", - internal=True, - refuri=f"#{element.fullname}", - anchorname="", - *[nodes.Text(element.label)], # noqa: B026 # (from original impl) - ) - para = addnodes.compact_paragraph("", "", ref) - if element.badge is not None: - ul.append(AttributeTableItem("", element.badge, para)) - else: - ul.append(AttributeTableItem("", para)) - - return AttributeTableColumn("", title, ul) - - -def setup(app: Sphinx) -> dict[str, Any]: - app.add_directive("attributetable", PyAttributeTable) - app.add_node(AttributeTable, html=(visit_attributetable_node, depart_attributetable_node)) - app.add_node(AttributeTableColumn, html=(visit_attributetablecolumn_node, depart_attributetablecolumn_node)) - app.add_node(AttributeTableTitle, html=(visit_attributetabletitle_node, depart_attributetabletitle_node)) - app.add_node(AttributeTableBadge, html=(visit_attributetablebadge_node, depart_attributetablebadge_node)) - app.add_node(AttributeTableItem, html=(visit_attributetable_item_node, depart_attributetable_item_node)) - app.add_node(AttributeTablePlaceholder) - _ = app.connect("doctree-resolved", process_attributetable) - return {"parallel_read_safe": True} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..52b0fb2a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,82 @@ +--- +hide: + - navigation +--- + + + +# Home + +
+ Logo + MCPROTO +
+ +## What is Mcproto + +Mcproto is a python library that provides various low level interactions with the minecraft protocol. It attempts to be +a full wrapper around the minecraft protocol, which means it could be used as a basis for minecraft bots written in +python, or even full python server implementations. + +!!! warning + + This library is still heavily Work-In-Progress, which means a lot of things can still change and some features may + be missing or incomplete. Using the library for production applications at it's current state isn't recommended. + +## Installation + +### PyPI (stable) version + +Mcproto is available on [PyPI](https://pypi.org/project/mcproto) and can be installed like any other python library with: + +=== "pip" + + ```bash + pip install mcproto + ``` + +=== "poetry" + + ```bash + poetry add mcproto + ``` + +=== "rye" + + ```bash + rye add mcproto + ``` + +### Latest (git) version + +Alternatively, you may want to install the latest available version, which is what you currently see in the `main` git +branch. Although this method will actually work for any branch with a pretty straightforward change. + +This kind of installation should only be done if you wish to test some new unreleased features and it's likely that you +will encounter bugs. + +That said, since mcproto is still in development, changes can often be made quickly and it can sometimes take a while +for these changes to carry over to PyPI. So if you really want to try out that latest feature, this is the method +you'll want. + +To install the latest mcproto version directly from the `main` git branch, use: + +=== "pip" + + ```bash + pip install 'mcproto@git+https://github.com/py-mine/mcproto@main' + ``` + +=== "poetry" + + ```bash + poetry add 'git+https://github.com/py-mine/mcproto#main' + ``` + +=== "rye" + + ```bash + rye add mcproto --git='https://github.com/py-mine/mcproto' --branch main + ``` diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 18170ab2..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,35 +0,0 @@ -.. mdinclude:: ../README.md - -Content -------- - -.. toctree:: - :maxdepth: 1 - :caption: Pages - - pages/installation.rst - usage/index.rst - examples/index.rst - pages/faq.rst - pages/changelog.rst - pages/version_guarantees.rst - pages/contributing.rst - pages/code-of-conduct.rst - -.. toctree:: - :maxdepth: 1 - :caption: API Documentation - - api/basic.rst - api/packets.rst - api/protocol.rst - api/internal.rst - api/types/index.rst - - -Indices and tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/pages/changelog.rst b/docs/pages/changelog.rst deleted file mode 100644 index 68f630ee..00000000 --- a/docs/pages/changelog.rst +++ /dev/null @@ -1,12 +0,0 @@ -Changelog -========= - -.. seealso:: - Check out what can and can't change between the library versions. :doc:`version_guarantees` - -.. attention:: - Major and minor releases also include the changes specified in prior development releases. - -.. towncrier-draft-entries:: Unreleased changes - -.. mdinclude:: ../../CHANGELOG.md diff --git a/docs/pages/code-of-conduct.rst b/docs/pages/code-of-conduct.rst deleted file mode 100644 index 20161955..00000000 --- a/docs/pages/code-of-conduct.rst +++ /dev/null @@ -1,159 +0,0 @@ -Code of Conduct -=============== - -This code of conduct outlines our expectations for the people involved with this project. We, as members, contributors -and leaders are committed to providing a welcoming and inspiring project that anyone can easily join, expecting -a harassment-free experience, as described in this code of conduct. - -This code of conduct is here to ensure we provide a welcoming and inspiring project that anyone can easily join, -expecting a harassment-free experience, as described in this code of conduct. - -The goal of this document is to set the overall tone for our community. It is here to outline some of the things you -can and can't do if you wish to participate in our community. However it is not here to serve as a rule-book with -a complete set of things you can't do, social conduct differs from situation to situation, and person to person, but we -should do our best to try and provide a good experience to everyone, in every situation. - -We value many things beyond just technical expertise, including collaboration and supporting others within our -community. Providing a positive experience for others can have a much more significant impact than simply providing the -correct answer. - -Harassment ----------- - -We share a common understanding of what constitutes harassment as it applies to a professional setting. Although this -list cannot be exhaustive, we explicitly honor diversity in age, gender, culture, ethnicity, language, national origin, -political beliefs, profession, race, religion, sexual orientation, socioeconomic status, disability and personal -appearance. We will not tolerate discrimination based on any of the protected characteristics above, including some -that may not have been explicitly mentioned here. We consider discrimination of any kind to be unacceptable and -immoral. - -Harassment includes, but is not limited to: - -* Offensive comments (or "jokes") related to any of the above mentioned attributes. -* Deliberate "outing"/"doxing" of any aspect of a person's identity, such as physical or electronic address, without - their explicit consent, except as necessary to protect others from intentional abuse. -* Unwelcome comments regarding a person's lifestyle choices and practices, including those related to food, health, - parenting, drugs and employment. -* Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a - person's gender identity. You must address people by the name they give you when not addressing them by their - username or handle. -* Threats of violence, both physical and psychological. -* Incitement of violence towards any individual, including encouraging a person to engage in self-harm. -* Publication of non-harassing private communication. -* Pattern of inappropriate social conduct, such as requesting/assuming inappropriate levels of intimacy with others, or - excessive teasing after a request to stop. -* Continued one-on-one communication after requests to cease. -* Sabotage of someone else's work or intentionally hindering someone else's performance. - -Plagiarism ----------- - -Plagiarism is the re-use of someone else's work (eg: binary content such as images, textual content such as an article, -but also source code, or any other copyrightable resources) without the permission or license right from the author. -Claiming someone else's work as your own is not just immoral and disrespectful to the author, but also illegal in most -countries. You should always follow the authors wishes, and give credit where credit is due. - -If we found that you've **intentionally** attempted to add plagiarized content to our code-base, you will likely end up -being permanently banned from any future contributions to this project's repository. We will of course also do our best -to remove, or properly attribute this plagiarized content as quickly as possible. - -An unintentional attempt at plagiarism will not be punished as harshly, but nevertheless, it is your responsibility as -a contributor to check where the code you're submitting comes from, and so repeated submission of such content, even -after you were warned might still get you banned. - -Please note that an online repository that has no license is presumed to only be source-available, NOT open-source. -Meaning that this work is protected by author's copyright, automatically imposed over it, and without any license -extending that copyright, you have no rights to use such code. So know that you can't simply take some source-code, -even though it's published publicly. This code may be available to be seen by anyone, but that does not mean it's also -available to be used by anyone in other projects. - -Another important note to keep in mind is that even if some project has an open-source license, that license may have -conditions which are incompatible with our codebase (such as requiring all of the code that links to this new part to -also be licensed under the same license, which our code-base is not currently under). That is why it's necessary to -understand a license before using code available under it. Simple attribution often isn't everything that the license -requires. - -Generally inappropriate behavior --------------------------------- - -Outside of just harassment and plagiarism, there are countless other behaviors which we consider unacceptable, as they -may be offensive, and discourage people from engaging with our community. - -**Examples of generally inappropriate behavior:** - -* The use of sexualized language or imagery of any kind -* The use of inappropriate images, including in an account's avatar -* The use of inappropriate language, including in an account's nickname -* Any spamming, flamming, baiting or other attention-stealing behavior -* Discussing topics that are overly polarizing, sensitive, or incite arguments. -* Responding with "RTFM", "just google it" or similar response to help requests -* Other conduct which could be reasonably considered inappropriate - -**Examples of generally appropriate behavior:** - -* Being kind and courteous to others -* Collaborating with other community members -* Gracefully accepting constructive criticism -* Using welcoming and inclusive language -* Showing empathy towards other community members - -Scope ------ - -This Code of Conduct applies within all community spaces, including this repository itself, conversations on any -platforms officially connected to this project (such as in GitHub issues, through official emails or applications like -discord). It also applies when an individual is officially representing the community in public spaces. Examples of -representing our community include using an official social media account, or acting as an appointed representative at -an online or offline event. - -All members involved with the project are expected to follow this Code of Conduct, no matter their position in the -project's hierarchy, this Code of Conduct applies equally to contributors, maintainers, people seeking help/reporting -bugs, etc. - -Enforcement Responsibilities ----------------------------- - -Whenever a participant has made a mistake, we expect them to take responsibility for their actions. If someone has been -harmed or offended, it is our responsibility to listen carefully and respectfully, and to do our best to right the -wrong. - -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take -appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, -offensive, harmful, or otherwise undesirable. - -Community leaders have the right and responsibility to remove, edit or reject comments, commits, code, wiki edits, -issues and other contributions within the enforcement scope that are not aligned to this Code of Conduct, and will -communicate reasons for moderation decisions when appropriate. - -If you have experienced or witnessed unacceptable behavior constituting a code of conduct violation or have any other -code of conduct concerns, please let us know and we will do our best to resolve this issue. - -Reporting a Code of Conduct violation -------------------------------------- - -If you saw someone violating the Code of Conduct in some way, you can report it to any repository maintainer, either by -email or through a Discord DM. You should avoid using public channels for reporting these, and instead do so in private -discussion with a maintainer. - -Sources -------- - -The open-source community has an incredible amount of resources that people have freely provided to others and we all -depend on these projects in many ways. This code of conduct article is no exception and there were many open source -projects that has helped bring this code of conduct to existence. For that reason, we'd like to thank all of these -communities and projects for keeping their content open and available to everyone, but most notably we'd like to thank -the projects with established codes of conduct and diversity statements that we used as our inspiration. Below is the -list these projects: - -* `Python `_ -* `Contributor Covenant `_ -* `Rust-lang `_ -* `Code Fellows `_ -* `Python Discord `_ - -License -------- - -All content of this page is licensed under a Creative Commons Attributions license. - -For more information about this license, see: diff --git a/docs/pages/contributing.rst b/docs/pages/contributing.rst deleted file mode 100644 index 1c554637..00000000 --- a/docs/pages/contributing.rst +++ /dev/null @@ -1,9 +0,0 @@ -Contributing Guidelines -======================= - -.. mdinclude:: ../../CONTRIBUTING.md - :start-line: 2 - -.. - TODO: Rewrite CONTRIBUTING.md here directly, rather than including it - like this, and just include a link to the docs in CONTRIBUTING.md diff --git a/docs/pages/faq.rst b/docs/pages/faq.rst deleted file mode 100644 index cb73d06a..00000000 --- a/docs/pages/faq.rst +++ /dev/null @@ -1,21 +0,0 @@ -Frequently Asked Questions -========================== - -.. note:: - This page is still being worked on, if you have any suggestions for a question, feel free to create an issue on - GitHub, or let us know on the development discord server. - -Missing synchronous alternatives for some functions ---------------------------------------------------- - -While mcproto does provide synchronous functionalities for the general protocol interactions (reading/writing packets -and lower level structures), any unrelated functionalities (such as HTTP interactions with the Minecraft API) will only -provide asynchronous versions. - -This was done to reduce the burden of maintaining 2 versions of the same code. The only reason protocol intercation -even have synchronous support is because it's needed in the :class:`~mcproto.buffer.Buffer` class. (See `Issue #128 -`_ for more details on this decision.) - -Generally, we recommend that you just stick to using the asynchronous alternatives though, both since some functions -only support async, and because async will generally provide you with a more scalable codebase, making it much easier -to handle multiple things concurrently. diff --git a/docs/pages/installation.rst b/docs/pages/installation.rst deleted file mode 100644 index 3515936b..00000000 --- a/docs/pages/installation.rst +++ /dev/null @@ -1,28 +0,0 @@ -Installation -============ - -PyPI (stable) version ---------------------- - -Mcproto is available on `PyPI `_, and can be installed trivially with: - -.. code-block:: bash - - python3 -m pip install mcproto - -This will install the latest stable (released) version. This is generally what you'll want to do. - -Latest (git) version --------------------- - -Alternatively, you may want to install the latest available version, which is what you currently see in the ``main`` -git branch. Although this method will actually work for any branch with a pretty straightforward change. This kind of -installation should only be done when testing new feautes, and it's likely you'll encounter bugs. - -That said, since mcproto is still in development, changes can often be made pretty quickly, and it can sometimes take a -while for these changes to carry over to PyPI. So if you really want to try out that latest feature, this is the method -you'll want. - -.. code-block:: bash - - python3 -m pip install 'mcproto@git+https://github.com/py-mine/mcproto@main' diff --git a/docs/pages/version_guarantees.rst b/docs/pages/version_guarantees.rst deleted file mode 100644 index d530fa86..00000000 --- a/docs/pages/version_guarantees.rst +++ /dev/null @@ -1,38 +0,0 @@ -Version Guarantees -================== - -.. attention:: - Mcproto is currently in the pre-release phase (pre v1.0.0). During this phase, these guarantees will NOT be - followed! This means that **breaking changes can occur in minor version bumps**, though micro version bumps are - still strictly for bugfixes, and will not include any features or breaking changes. - -This library follows `semantic versioning model `_, which means the major version is updated every -time there is an incompatible (breaking) change made to the public API. However due to the fairly dynamic nature of -Python, it can be hard to discern what can be considered a breaking change, and what isn't. - -First thing to keep in mind is that breaking changes only apply to **publicly documented functions and classes**. If -it's not listed in the documentation here, it's an internal feature, that isn't considered a part of the public API, -and thus is bound to change. This includes documented attributes that start with an underscore. - -.. note:: - The examples below are non-exhaustive. - -Examples of Breaking Changes ----------------------------- - -* Changing the default parameter value of a function to something else. -* Renaming (or removing) a function without an alias to the old function. -* Adding or removing parameters of a function. -* Removing deprecated alias to a renamed function - -Examples of Non-Breaking Changes --------------------------------- - -* Changing function's name, while providing a deprecated alias. -* Renaming (or removing) private underscored attributes. -* Adding an element into `__slots__` of a data class. -* Changing the behavior of a function to fix a bug. -* Changes in the typing behavior of the library. -* Changes in the documentation. -* Modifying the internal protocol connection handling. -* Updating the dependencies to a newer version, major or otherwise. diff --git a/docs/usage/authentication.rst b/docs/usage/authentication.rst deleted file mode 100644 index 6adb328f..00000000 --- a/docs/usage/authentication.rst +++ /dev/null @@ -1,268 +0,0 @@ -Minecraft account authentication -================================ - -Mcproto has first party support to handle authentication, allowing you to use your own minecraft account. This is -needed if you wish to login to "online mode" (non-warez) servers as a client (player). - -Microsoft (migrated) accounts ------------------------------ - -This is how authentication works for already migrated minecraft accounts, using Microsoft accounts for authentication. -(This will be most accounts. Any newly created minecraft accounts - after 2021 will always be Microsoft linked accounts.) - -Creating Azure application -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To authenticate with a microsoft account, you will need to go through the entire OAuth2 flow. Mcproto has functions to -hide pretty much all of this away, however you will need to create a new Microsoft Azure application, that mcproto will -use to obtain an access token. - -We know this is annoying, but it's a necessary step, as Microsoft only allows these applications to request OAuth2 -authentication, and to avoid potential abuse, we can't really just use our registered application (like with say -`MultiMC `_), as this token would have to be embedded into our source-code, and -since this is python, that would mean just including it here in plain text, and because mcproto is a low level library -that can be used for any kind of interactions, we can't trust that you won't abuse this token. - -Instead, everyone using mcproto should register a new application, and get their own MSA token for your application -that uses mcproto in the back. - -To create a new application, follow these steps (this is a simplified guide, for a full guide, feel free to check the -`Microsoft documentation `): - -#. Go to the `Azure portal `_ and log in (create an account if you need to). -#. Search for and select **Azure Active Directory**. -#. On the left navbar, under **Manage** section, click on **App registrations**. -#. Click on **New registration** on top navbar. -#. Pick a name for the application. Anyone using your app to authenticate will see this name. -#. Choose **Personal Microsoft accounts only** from the Supported account types. -#. Leave the **Redirect URI (optional)** empty. -#. Click on **Register**. - -From there, you will need to enable this application to be used for OAuth2 flows. To do that, follow these steps: - -#. On the left navbar, under **Manage** section, click on **Authentication**. -#. Set **Allow public content flows** to **Yes**. -#. Click **Save**. - -After that, you can go back to the app (click **Overview** from the left navbar), and you'll want to copy the -**Application (client) ID**. This is the ID you will need to pass to mcproto. (You will also need the **Display name**, -and the **Directory (Tenant) ID** for `Registering the application with Minecraft`_ - first time only) - -If you ever need to access this application again, follow these steps (as Microsoft Azure is pretty unintuitive, we -document this too): - -#. Go to the `Azure portal `_ and log in. -#. Click on **Azure Active Directory** (if you can't find it on the main page, you can use the search). -#. On the left navbar, under **Manage** section, click on **App registrations**. -#. Click on **View all applications from personal account** (assuming you registered the app from a personal account). -#. Click on your app. - -Registering the application with Minecraft -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, this step wasn't required, however due to people maliciously creating these applications to steal accounts, -Mojang have recently started to limit access to the ``https://api.minecraftservices.com``, and only allow explicitly -white listed Client IDs to use this API. - -This API is absolutely crucial step in getting the final minecraft token, and so you will need to register your Client -ID to be white listed by Mojang. Thankfully, it looks like Mojang is generally pretty lenient and at least for me, they -didn't cause any unnecessary hassles when I asked for my application to be registered, for development purposes and -work on mcproto. - -That said, you will need to wait a while (about a week, though it could be more), until Mojang reviews your application -and approves it. There isn't much we can do about this. - -To get your Azure application registered, you will need to fill out a simple form, where you accept the EULA, provide -your E-Mail, Application name, Application Client ID and Tennant ID. - -More annoyingly you will additionally also need to provide an **associated website or domain** for your project/brand. -(This application is generally designed for more user-facing programs, such as full launchers. When registering -mcproto, I just used the GitHub URL). Lastly, you'll want to describe why you need access to this API in the -**Justification** section. - -Visit the `mojang article `_ describing this process. -There is also a link to the form to fill out. - - -The code -^^^^^^^^ - -Finally, after you've managed to register your application and get it approved by Mojang, you can use it with mcproto, -go through the Microsoft OAuth2 flow and authorize this application to access your Microsoft account, which mcproto -will then use to get the minecraft token you'll then need to login to online servers. - -.. code-block:: python - - import httpx - from mcproto.auth.microsoft.oauth import full_microsoft_oauth - from mcproto.auth.microsoft.xbox import xbox_auth - from mcproto.auth.msa import MSAAccount - - MY_MSA_CLIENT_ID = "[REDACTED]" # Paste your own Client ID here - - async def authenticate() -> MSAAccount: - async with httpx.AsyncClient() as client: - microsoft_token = await full_microsoft_oauth(client, MY_MSA_CLIENT_ID) - user_hash, xsts_token = xbox_auth(client, microsoft_token) - return MSAAccount.xbox_auth(cilent, user_hash, xsts_token) - -Note that the :meth:`~mcproto.auth.microsoft.oauth.full_microsoft_oauth` will print a message containing the URL you -should visit in your browser, and a one time code to type in once you reach this URL. That will then prompt you to log -in to your Microsoft account, and then allow you to authorize the application to use your account. - -Caching -^^^^^^^ - -You will very likely want to set up caching here, and store at least the ``microsoft_token`` somewhere, so you don't -have to log in each time your code will run. Here's some example code that caches every step of the way, always -resorting to the "closest" functional token. Note that this is using `pickle` to store the tokens, you may want to use -JSON or other format instead, as it would be safer. Also, be aware that these are sensitive and if compromised, someone -could gain access to your minecraft account (though only for playing, they shouldn't be able to change your password or -anything like that), so you might want to consider encrypting these cache files before storing: - -.. code-block:: python - - from __future__ import annotations - - import logging - import pickle - from pathlib import Path - - import httpx - - from mcproto.auth.microsoft.oauth import full_microsoft_oauth - from mcproto.auth.microsoft.xbox import XSTSRequestError, xbox_auth - from mcproto.auth.msa import MSAAccount, ServicesAPIError - - log = logging.getLogger(__name__) - - MY_MSA_CLIENT_ID = "[REDACTED]" # Paste your own Client ID here - CACHE_DIR = Path(".cache/") - - - async def microsoft_login(client: httpx.AsyncClient) -> MSAAccount: # noqa: PLR0912,PLR0915 - """Obtain minecraft account using Microsoft authentication. - - This function performs full caching of every step along the way, allowing for recovery - without manual intervention for as long as at least the root token (from Microsoft OAuth2) - is valid. Any later tokens will be refreshed and re-cached once invalid. - - If all tokens are invalid, or this function was ran for the first time (without any cached - data), you will be shown a URL and a code. You have to go to this URL with your browser and - enter the code, completing the OAuth2 flow, obtaining the root token. - """ - CACHE_DIR.mkdir(parents=True, exist_ok=True) - - access_token_cache = CACHE_DIR.joinpath("xbox_access_token.pickle") - if access_token_cache.exists(): - with access_token_cache.open("rb") as f: - access_token: str = pickle.load(f) # noqa: S301 - - try: - account = await MSAAccount.from_xbox_access_token(client, access_token) - log.info("Logged in with cached xbox minecraft access token") - return account - except httpx.HTTPStatusError as exc: - log.warning(f"Cached xbox minecraft access token is invalid: {exc!r}") - else: - log.warning("No cached access token available, trying Xbox Secure Token Service (XSTS) token") - - # Access token either doesn't exist, or isn't valid, try XSTS (Xbox) token - xbox_token_cache = CACHE_DIR.joinpath("xbox_xsts_token.pickle") - if xbox_token_cache.exists(): - with xbox_token_cache.open("rb") as f: - user_hash, xsts_token = pickle.load(f) # noqa: S301 - - try: - access_token = await MSAAccount._get_access_token_from_xbox(client, user_hash, xsts_token) - except ServicesAPIError as exc: - log.warning(f"Invalid cached Xbox Secure Token Service (XSTS) token: {exc!r}") - else: - log.info("Obtained xbox access token from cached Xbox Secure Token Service (XSTS) token") - log.info("Storing xbox minecraft access token to cache and restarting auth") - with access_token_cache.open("wb") as f: - pickle.dump(access_token, f) - return await microsoft_login(client) - else: - log.warning("No cached Xbox Secure Token Service (XSTS) token available, trying Microsoft OAuth2 token") - - # XSTS token either doesn't exist, or isn't valid, try Microsoft OAuth2 token - microsoft_token_cache = CACHE_DIR.joinpath("microsoft_token.pickle") - if microsoft_token_cache.exists(): - with microsoft_token_cache.open("rb") as f: - microsoft_token = pickle.load(f) # noqa: S301 - - try: - user_hash, xsts_token = await xbox_auth(client, microsoft_token) - except (httpx.HTTPStatusError, XSTSRequestError) as exc: - log.warning(f"Invalid cached Microsoft OAuth2 token {exc!r}") - else: - log.info("Obtained Xbox Secure Token Service (XSTS) token from cached Microsoft OAuth2 token") - log.info("Storing Xbox Secure Token Service (XSTS) token to cache and restarting auth") - with xbox_token_cache.open("wb") as f: - pickle.dump((user_hash, xsts_token), f) - return await microsoft_login(client) - else: - log.warning("No cached microsoft token") - - # Microsoft OAuth2 token either doesn't exist, or isn't valid, request user auth - log.info("Running Microsoft OAuth2 flow, requesting user authentication") - microsoft_token = await full_microsoft_oauth(client, MY_MSA_CLIENT_ID) - log.info("Obtained Microsoft OAuth2 token from user authentication") - log.info("Storing Microsoft OAuth2 token and restarting auth") - with microsoft_token_cache.open("wb") as f: - pickle.dump(microsoft_token["access_token"], f) - return await microsoft_login(client) - -Minecraft (non-migrated) accounts ---------------------------------- - -If you still haven't migrated your account and linked it to a Microsoft account, follow this guide for authentication. -(Any newly created Minecraft accounts will be using Microsoft accounts already.) This method of authentication is -called "yggdrasil". - -.. warning:: - Mojang has announced that they will be closing the migration period for these unmigrated accounts in **September - 19, 2023**. See: ``_ - - Once that happen, any unmigrated accounts will no longer work, and you won't be able to log in. If you're still - using an unmigrated account, it's about time to move. - - Mcproto will remove support for this old authentication methods once this happens. - -This method of authentication doesn't require any special app registrations, however it is significantly less secure, -as you need to enter your login and password directly. - -.. code-block:: python - - import httpx - from mcproto.auth.yggdrasil import YggdrasilAccount - - LOGIN = "mail@example.com" - PASSWORD = "my_password" - - async def authenticate() -> YggdrasilAccount: - async with httpx.AsyncClient() as client: - return YggdrasilAccount.authenticate(client, login=LOGIN, password=PASSWORD) - - -The Account instance you will obtain here will contain a refresh token, and a shorter lived access token, received from -Mojang APIs from the credentials you entered. Just like with Microsoft accounts, you may want to cache these tokens to -avoid needless calls to request new ones and go through authentication again. That said, since doing so doesn't -necessarily require user interaction, if you make the credentials accessible from your code directly, this is a lot -less annoying. - -If you will decide to use caching, or if you plan on using these credentials in a long running program, you may see the -access token expire. You can check whether the token is expired with the -:meth:`~mcproto.auth.yggdrasil.YggdrasilAccount.validate` method, and if it is (call returned ``False``), you can call -:meth:`~mcproto.auth.yggdrasil.YggdrasilAccount.refresh` to use the refresh token to obtain a new access token. The -refresh token is much more long lived than the access token, so this should generally be enough for you, although if -you login from elsewhere, or after a really long time, the refresh token might be invalidated, in that case, you'll -need to go through the full login again. - -Legacy Mojang accounts ----------------------- - -If your minecraft account is still using the (really old) Mojang authentication, you can simply follow the non-migrated -guide, as it will work with these legacy accounts too, the only change you will need to make is to use your username, -instead of an email. diff --git a/docs/usage/index.rst b/docs/usage/index.rst deleted file mode 100644 index 430806d7..00000000 --- a/docs/usage/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Usage guides -============ - -Here are some guides and explanations on how to use the various different parts of mcproto. - - -.. toctree:: - :maxdepth: 1 - :caption: Guides - - authentication.rst - -Feel free to propose any further guides, we'll be happy to add them to the list! diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..a5608e10 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,69 @@ +site_name: MCPROTO +site_url: https://py-mine.github.io/mcproto + +repo_url: https://github.com/py-mine/mcproto +repo_name: py-mine/mcproto + +nav: + - Home: index.md + +theme: + name: material + logo: assets/py-mine_logo.png + palette: + - media: "(prefers-color-scheme)" + primary: black + accent: black + toggle: + icon: material/brightness-auto + name: Switch to light mode + + - media: "(prefers-color-scheme: light)" + scheme: default + primary: black + accent: black + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: black + toggle: + icon: material/brightness-4 + name: Switch to system preference + icon: + repo: fontawesome/brands/github + features: + - content.tabs.link + - content.code.copy + - content.action.edit + - search.highlight + - search.share + - search.suggest + - navigation.footer + - navigation.indexes + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - toc.follow + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - toc: + permalink: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets + - pymdownx.details + - pymdownx.tabbed: + alternate_style: true + +plugins: + - search diff --git a/poetry.lock b/poetry.lock index a3b42b90..d82aab2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,16 +1,5 @@ # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. -[[package]] -name = "alabaster" -version = "0.7.16" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.9" -files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, -] - [[package]] name = "anyio" version = "4.3.0" @@ -97,27 +86,6 @@ files = [ [package.dependencies] nodejs-wheel-binaries = ">=20.13.1" -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - [[package]] name = "certifi" version = "2024.7.4" @@ -470,17 +438,6 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] -[[package]] -name = "docutils" -version = "0.19" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, -] - [[package]] name = "dunamai" version = "1.21.1" @@ -526,21 +483,21 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", typing = ["typing-extensions (>=4.8)"] [[package]] -name = "furo" -version = "2024.8.6" -description = "A clean customisable Sphinx documentation theme." +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." optional = false -python-versions = ">=3.8" +python-versions = "*" files = [ - {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, - {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] [package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7" -sphinx = ">=6.0,<9.0" -sphinx-basic-ng = ">=1.0.0.beta2" +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "h11" @@ -622,17 +579,6 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - [[package]] name = "importlib-metadata" version = "7.1.0" @@ -717,19 +663,22 @@ MarkupSafe = ">=2.0" i18n = ["Babel (>=2.7)"] [[package]] -name = "m2r2" -version = "0.3.3.post2" -description = "Markdown and reStructuredText in a single file." +name = "markdown" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "m2r2-0.3.3.post2-py3-none-any.whl", hash = "sha256:86157721eb6eabcd54d4eea7195890cc58fa6188b8d0abea633383cfbb5e11e3"}, - {file = "m2r2-0.3.3.post2.tar.gz", hash = "sha256:e62bcb0e74b3ce19cda0737a0556b04cf4a43b785072fcef474558f2c1482ca8"}, + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] [package.dependencies] -docutils = ">=0.19" -mistune = "0.8.4" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" @@ -801,14 +750,102 @@ files = [ ] [[package]] -name = "mistune" -version = "0.8.4" -description = "The fastest markdown parser in pure Python" +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." optional = false -python-versions = "*" +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.6.2" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.6.2-py3-none-any.whl", hash = "sha256:71d90dbd63b393ad11a4d90151dfe3dcbfcd802c0f29ce80bebd9bbac6abc753"}, + {file = "mkdocs_material-9.6.2.tar.gz", hash = "sha256:a3de1c5d4c745f10afa78b1a02f917b9dce0808fb206adc0f5bb48b58c1ca21f"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" files = [ - {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, - {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, ] [[package]] @@ -863,6 +900,32 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "platformdirs" version = "4.2.2" @@ -984,6 +1047,24 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymdown-extensions" +version = "10.14.3" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, + {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.19.1)"] + [[package]] name = "pytest" version = "7.4.4" @@ -1060,6 +1141,20 @@ pytest = ">=6.0,<8.0" [package.extras] testing = ["pytest-asyncio (==0.21.*)", "pytest-cov (==4.*)"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pyyaml" version = "6.0.1" @@ -1120,6 +1215,123 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + [[package]] name = "requests" version = "2.32.2" @@ -1183,6 +1395,17 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "slotscheck" version = "0.19.1" @@ -1209,223 +1432,6 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "sphinx" -version = "7.3.7" -description = "Python documentation generator" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, - {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, -] - -[package.dependencies] -alabaster = ">=0.7.14,<0.8.0" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.22" -imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.14" -requests = ">=2.25.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "2.3.0" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinx_autodoc_typehints-2.3.0-py3-none-any.whl", hash = "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67"}, - {file = "sphinx_autodoc_typehints-2.3.0.tar.gz", hash = "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084"}, -] - -[package.dependencies] -sphinx = ">=7.3.5" - -[package.extras] -docs = ["furo (>=2024.1.29)"] -numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] - -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b2" -description = "A modern skeleton for Sphinx themes." -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, - {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, -] - -[package.dependencies] -sphinx = ">=4.0" - -[package.extras] -docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -description = "Add a copy button to each of your code cells." -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, - {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, -] - -[package.dependencies] -sphinx = ">=1.8" - -[package.extras] -code-style = ["pre-commit (==2.12.1)"] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.4" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.1" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-towncrier" -version = "0.4.0a0" -description = "An RST directive for injecting a Towncrier-generated changelog draft containing fragments for the unreleased (next) project version" -optional = false -python-versions = ">=3.6" -files = [ - {file = "sphinxcontrib-towncrier-0.4.0a0.tar.gz", hash = "sha256:d9b1513fc07781432dd3a0b2ca797cfe0e99e9b5bc5e5c8bf112d5d142afb6dc"}, - {file = "sphinxcontrib_towncrier-0.4.0a0-py3-none-any.whl", hash = "sha256:ec734e3d0920e2ce26e99681119f398a9e1fc0aa6c2d7ed1f052f1219dcd4653"}, -] - -[package.dependencies] -sphinx = "*" -towncrier = ">=19.2" - [[package]] name = "taskipy" version = "1.14.1" @@ -1564,6 +1570,48 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "zipp" version = "3.19.1" @@ -1582,4 +1630,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "020f23a88ba315f01428242a3afe7ee3e02041f1bc371e8e5b0ad723aeac451c" +content-hash = "d7e0eb825682cbf05096941953bb9cd7084125d101387526872e401adaae238b" diff --git a/pyproject.toml b/pyproject.toml index 1b184a98..27fae033 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,15 +63,8 @@ optional = true poetry-dynamic-versioning = ">=1.4.0,<1.8" [tool.poetry.group.docs.dependencies] -sphinx = ">=6.2.1,<8.0.0" -tomli = { version = "^2.0.1", python = "<3.11" } -m2r2 = "^0.3.3.post2" -packaging = ">=23.1,<25.0" -sphinx-autodoc-typehints = ">=1.23,<3.0" -sphinx-copybutton = "^0.5.2" -furo = ">=2022.12.7" -sphinxcontrib-towncrier = ">=0.3.2,<0.5.0" -pytest = "^7.3.1" # Required to import the gen_test_serializable function to list it in the docs +mkdocs = "^1.6.0" +mkdocs-material = "^9.5.30" [tool.poetry.group.docs-ci] optional = true From 1a46a23aab1d441f71dbbb5aa0f233c2e3631d74 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 09:57:49 +0200 Subject: [PATCH 02/85] Add mike for mkdocs versioning --- .github/workflows/mkdocs.yml | 36 ++++++++--------------- .github/workflows/publish.yml | 46 +++++++++++++++++++++++++++++ mkdocs.yml | 9 ++++++ poetry.lock | 55 ++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 122 insertions(+), 25 deletions(-) diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 4425b36b..cd81c0f4 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -31,6 +31,9 @@ jobs: with: token: "${{ steps.app-token.outputs.token }}" + - name: Fetch gh-pages branch + run: git fetch origin gh-pages + # Make the github application be the committer # (see: https://stackoverflow.com/a/74071223 on how to obtain the committer email) - name: Setup git config @@ -45,25 +48,11 @@ jobs: python-version: 3.12 install-args: "--only main,docs" - - name: Generate docs directory hash - run: | - docs_dir_hash="$(find "./docs" -type f -exec sha256sum {} + | sort | sha256sum | awk '{print $1}')" - echo "docs_dir_hash=$docs_hash" >> $GITHUB_ENV - - - name: Restore MkDocs cache - uses: actions/cache@v4 - with: - path: .cache - key: - "mkdocs-material-${{ steps.poetry_setup.outputs.python-version }}-\ - ${{ hashFiles('./poetry.lock') }}-${{ hashFiles('./mkdocs.yml') }}-\ - ${{ env.docs_dir_hash }}" - restore-keys: "mkdocs-material-${{ steps.poetry_setup.outputs.python-version }}-" - - - name: Build the documentation + - name: Build the documentation (mkdocs - PR preview) + if: ${{ github.event_name == 'pull_request' }} run: poetry run mkdocs build - - name: Deploy preview + - name: Deploy docs - PR preview if: ${{ github.event_name == 'pull_request' }} uses: rossjrw/pr-preview-action@v1 with: @@ -72,11 +61,10 @@ jobs: umbrella-dir: pr-preview token: ${{ steps.app-token.outputs.token }} - - name: Deploy production + - name: Build the documentation (mike) if: ${{ github.event_name == 'push' }} - uses: JamesIves/github-pages-deploy-action@v4 - with: - branch: gh-pages - folder: ./site - clean-exclude: pr-preview/ - token: ${{ steps.app-token.outputs.token }} + run: poetry run mike deploy latest + + - name: Deploy docs - latest + if: ${{ github.event_name == 'push' }} + run: git push origin gh-pages diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cfbd4c77..4247678b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -132,3 +132,49 @@ jobs: # This uses PyPI's trusted publishing, so no token is required - name: Release to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + publish-docs: + name: "Publish release docs" + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Generate token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: "${{ steps.app-token.outputs.token }}" + + - name: Fetch gh-pages branch + run: git fetch origin gh-pages + + # Make the github application be the committer + # (see: https://stackoverflow.com/a/74071223 on how to obtain the committer email) + - name: Setup git config + run: | + git config --global user.name "py-mine-ci-bot" + git config --global user.email "121461646+py-mine-ci-bot[bot]@users.noreply.github.com" + + - name: Setup poetry + id: poetry_setup + uses: ItsDrike/setup-poetry@v1 + with: + python-version: 3.12 + install-args: "--only main,docs,release-ci" + + - name: Set version with dynamic versioning + run: poetry run poetry-dynamic-versioning + + - name: Build the documentation (mike) + run: poetry run mike deploy --update-aliases "$(poetry version --short)" release + + - name: Deploy docs - release + run: git push origin gh-pages diff --git a/mkdocs.yml b/mkdocs.yml index a5608e10..dadfdc83 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,3 +67,12 @@ markdown_extensions: plugins: - search + - mike: + canonical_version: "latest" + version_selector: true + +extra: + version: + provider: mike + default: latest + alias: true diff --git a/poetry.lock b/poetry.lock index d82aab2e..83950537 100644 --- a/poetry.lock +++ b/poetry.lock @@ -760,6 +760,31 @@ files = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] +[[package]] +name = "mike" +version = "2.1.3" +description = "Manage multiple versions of your MkDocs-powered documentation" +optional = false +python-versions = "*" +files = [ + {file = "mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a"}, + {file = "mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810"}, +] + +[package.dependencies] +importlib-metadata = "*" +importlib-resources = "*" +jinja2 = ">=2.7" +mkdocs = ">=1.0" +pyparsing = ">=3.0" +pyyaml = ">=5.1" +pyyaml-env-tag = "*" +verspec = "*" + +[package.extras] +dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] +test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] + [[package]] name = "mkdocs" version = "1.6.1" @@ -1065,6 +1090,20 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.19.1)"] +[[package]] +name = "pyparsing" +version = "3.2.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "7.4.4" @@ -1550,6 +1589,20 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "verspec" +version = "0.1.0" +description = "Flexible version handling" +optional = false +python-versions = "*" +files = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] + +[package.extras] +test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] + [[package]] name = "virtualenv" version = "20.26.6" @@ -1630,4 +1683,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "d7e0eb825682cbf05096941953bb9cd7084125d101387526872e401adaae238b" +content-hash = "347422a4ca864526c3a004a89f995af02799640bea7819ad6c712ed98ee2888f" diff --git a/pyproject.toml b/pyproject.toml index 27fae033..9a26182a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ poetry-dynamic-versioning = ">=1.4.0,<1.8" [tool.poetry.group.docs.dependencies] mkdocs = "^1.6.0" mkdocs-material = "^9.5.30" +mike = "^2.1.2" [tool.poetry.group.docs-ci] optional = true From 05652da6752acd8c16862c06f169a83e67f50c2c Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 10:11:50 +0200 Subject: [PATCH 03/85] Add changelog fragment for 346 --- changes/346.docs.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/346.docs.md diff --git a/changes/346.docs.md b/changes/346.docs.md new file mode 100644 index 00000000..2551f06d --- /dev/null +++ b/changes/346.docs.md @@ -0,0 +1 @@ +Complete documentation rewrite From b17c3128eeaa7341bb68269d7a2c500994109275 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 10:31:03 +0200 Subject: [PATCH 04/85] Split off the installation page to its own tab --- docs/index.md | 64 ++------------------------------------ docs/installation.md | 55 ++++++++++++++++++++++++++++++++ docs/version_guarantees.md | 37 ++++++++++++++++++++++ mkdocs.yml | 3 ++ 4 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 docs/installation.md create mode 100644 docs/version_guarantees.md diff --git a/docs/index.md b/docs/index.md index 52b0fb2a..2aceda6e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,10 +3,6 @@ hide: - navigation --- - - # Home
@@ -16,67 +12,11 @@ vi: tw=119 ## What is Mcproto -Mcproto is a python library that provides various low level interactions with the minecraft protocol. It attempts to be -a full wrapper around the minecraft protocol, which means it could be used as a basis for minecraft bots written in +Mcproto is a python library that provides various low level interactions with the Minecraft protocol. It attempts to be +a full wrapper around the Minecraft protocol, which means it could be used as a basis for Minecraft bots written in python, or even full python server implementations. !!! warning This library is still heavily Work-In-Progress, which means a lot of things can still change and some features may be missing or incomplete. Using the library for production applications at it's current state isn't recommended. - -## Installation - -### PyPI (stable) version - -Mcproto is available on [PyPI](https://pypi.org/project/mcproto) and can be installed like any other python library with: - -=== "pip" - - ```bash - pip install mcproto - ``` - -=== "poetry" - - ```bash - poetry add mcproto - ``` - -=== "rye" - - ```bash - rye add mcproto - ``` - -### Latest (git) version - -Alternatively, you may want to install the latest available version, which is what you currently see in the `main` git -branch. Although this method will actually work for any branch with a pretty straightforward change. - -This kind of installation should only be done if you wish to test some new unreleased features and it's likely that you -will encounter bugs. - -That said, since mcproto is still in development, changes can often be made quickly and it can sometimes take a while -for these changes to carry over to PyPI. So if you really want to try out that latest feature, this is the method -you'll want. - -To install the latest mcproto version directly from the `main` git branch, use: - -=== "pip" - - ```bash - pip install 'mcproto@git+https://github.com/py-mine/mcproto@main' - ``` - -=== "poetry" - - ```bash - poetry add 'git+https://github.com/py-mine/mcproto#main' - ``` - -=== "rye" - - ```bash - rye add mcproto --git='https://github.com/py-mine/mcproto' --branch main - ``` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 00000000..58651b28 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,55 @@ +# Installation + +## PyPI (stable) version + +Mcproto is available on [PyPI](https://pypi.org/project/mcproto) and can be installed like any other python library with: + +=== "pip" + + ```bash + pip install mcproto + ``` + +=== "poetry" + + ```bash + poetry add mcproto + ``` + +=== "rye" + + ```bash + rye add mcproto + ``` + +## Latest (git) version + +Alternatively, you may want to install the latest available version, which is what you currently see in the `main` git +branch. Although this method will actually work for any branch with a pretty straightforward change. + +This kind of installation should only be done if you wish to test some new unreleased features and it's likely that you +will encounter bugs. + +That said, since mcproto is still in development, changes can often be made quickly and it can sometimes take a while +for these changes to carry over to PyPI. So if you really want to try out that latest feature, this is the method +you'll want. + +To install the latest mcproto version directly from the `main` git branch, use: + +=== "pip" + + ```bash + pip install 'mcproto@git+https://github.com/py-mine/mcproto@main' + ``` + +=== "poetry" + + ```bash + poetry add 'git+https://github.com/py-mine/mcproto#main' + ``` + +=== "rye" + + ```bash + rye add mcproto --git='https://github.com/py-mine/mcproto' --branch main + ``` diff --git a/docs/version_guarantees.md b/docs/version_guarantees.md new file mode 100644 index 00000000..8a64b7c9 --- /dev/null +++ b/docs/version_guarantees.md @@ -0,0 +1,37 @@ +# Version Guarantees + +!!! danger "Pre-release phase" + + Mcproto is currently in the pre-release phase (pre v1.0.0). During this phase, these guarantees will NOT be + followed! This means that **breaking changes can occur in minor version bumps**. That said, micro version bumps are + still strictly for bugfixes, and will not include any features or breaking changes. + +This library follows [semantic versioning model](https://semver.org), which means the major version is updated every time +there is an incompatible (breaking) change made to the public API. However due to the fairly dynamic nature of Python, +it can be hard to discern what can be considered a breaking change, and what isn't. + +First thing to keep in mind is that breaking changes only apply to **publicly documented functions and classes**. If +it's not listed in the documentation here, it's an internal feature, that isn't considered a part of the public API, +and thus is bound to change. This includes documented attributes that start with an underscore. + +!!! note + + The examples below are non-exhaustive. + +## Examples of Breaking Changes + +- Changing the default parameter value of a function to something else. +- Renaming (or removing) a function without an alias to the old function. +- Adding or removing parameters of a function. +- Removing deprecated alias to a renamed function + +## Examples of Non-Breaking Changes + +- Changing function's name, while providing a deprecated alias. +- Renaming (or removing) private underscored attributes. +- Adding an element into `__slots__` of a data class. +- Changing the behavior of a function to fix a bug. +- Changes in the typing behavior of the library. +- Changes in the documentation. +- Modifying the internal protocol connection handling. +- Updating the dependencies to a newer version, major or otherwise. diff --git a/mkdocs.yml b/mkdocs.yml index dadfdc83..11bb22af 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,9 @@ repo_name: py-mine/mcproto nav: - Home: index.md + - Installation: + - Installation: installation.md + - Version Guarantees: version_guarantees.md theme: name: material From 936dc21305b62c6c1f499d6c7bf862752c7d4c99 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 11:54:38 +0200 Subject: [PATCH 05/85] Add changelog file (incomplete) --- docs/changelog.md | 5 +++++ mkdocs.yml | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/changelog.md diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..0789f663 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,5 @@ +!!! danger "" + + Major and minor releases also include the changes specified in prior development releases. + +TODO: Include changelog + find a way to have towncrier generate unreleased changes diff --git a/mkdocs.yml b/mkdocs.yml index 11bb22af..6a4bacec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - Installation: - Installation: installation.md - Version Guarantees: version_guarantees.md + - Changelog: changelog.md theme: name: material From 371927eeba6f3f1c7ae0e07179243bb4b7fdf489 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 11:55:04 +0200 Subject: [PATCH 06/85] Add code of conduct and license pages --- docs/code_of_conduct.md | 155 ++++++++++++++++++++++++++++++++++++++++ docs/license.md | 22 ++++++ mkdocs.yml | 3 + 3 files changed, 180 insertions(+) create mode 100644 docs/code_of_conduct.md create mode 100644 docs/license.md diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md new file mode 100644 index 00000000..a5d1fe0c --- /dev/null +++ b/docs/code_of_conduct.md @@ -0,0 +1,155 @@ +# Code of Conduct + +This code of conduct outlines our expectations for the people involved with this project. We, as members, contributors +and leaders are committed to providing a welcoming and inspiring project that anyone can easily join, expecting +a harassment-free experience, as described in this code of conduct. + +This code of conduct is here to ensure we provide a welcoming and inspiring project that anyone can easily join, +expecting a harassment-free experience, as described in this code of conduct. + +The goal of this document is to set the overall tone for our community. It is here to outline some of the things you +can and can't do if you wish to participate in our community. However it is not here to serve as a rule-book with +a complete set of things you can't do, social conduct differs from situation to situation, and person to person, but we +should do our best to try and provide a good experience to everyone, in every situation. + +We value many things beyond just technical expertise, including collaboration and supporting others within our +community. Providing a positive experience for others can have a much more significant impact than simply providing the +correct answer. + +## Harassment + +We share a common understanding of what constitutes harassment as it applies to a professional setting. Although this +list cannot be exhaustive, we explicitly honor diversity in age, gender, culture, ethnicity, language, national origin, +political beliefs, profession, race, religion, sexual orientation, socioeconomic status, disability and personal +appearance. We will not tolerate discrimination based on any of the protected characteristics above, including some +that may not have been explicitly mentioned here. We consider discrimination of any kind to be unacceptable and +immoral. + +Harassment includes, but is not limited to: + +- Offensive comments (or "jokes") related to any of the above mentioned attributes. +- Deliberate "outing"/"doxing" of any aspect of a person's identity, such as physical or electronic address, without + their explicit consent, except as necessary to protect others from intentional abuse. +- Unwelcome comments regarding a person's lifestyle choices and practices, including those related to food, health, + parenting, drugs and employment. +- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a + person's gender identity. You must address people by the name they give you when not addressing them by their + username or handle. +- Threats of violence, both physical and psychological. +- Incitement of violence towards any individual, including encouraging a person to engage in self-harm. +- Publication of non-harassing private communication. +- Pattern of inappropriate social conduct, such as requesting/assuming inappropriate levels of intimacy with others, or + excessive teasing after a request to stop. +- Continued one-on-one communication after requests to cease. +- Sabotage of someone else's work or intentionally hindering someone else's performance. + +## Plagiarism + +Plagiarism is the re-use of someone else's work (eg: binary content such as images, textual content such as an article, +but also source code, or any other copyrightable resources) without the permission or a license right from the author. +Claiming someone else's work as your own is not just immoral and disrespectful to the author, but also illegal in most +countries. You should always follow the authors wishes, and give credit where credit is due. + +If we found that you've **intentionally** attempted to add plagiarized content to our code-base, you will likely end up +being permanently banned from any future contributions to this project's repository. We will of course also do our best +to remove, or properly attribute this plagiarized content as quickly as possible. + +An unintentional attempt at plagiarism will not be punished as harshly, but nevertheless, it is your responsibility as +a contributor to check where the code you're submitting comes from, and so repeated submission of such content, even +after you were warned might still get you banned. + +Please note that an online repository that has no license is presumed to only be source-available, NOT open-source. +Meaning that this work is protected by author's copyright, automatically imposed over it, and without any license +extending that copyright, you have no rights to use such code. So know that you can't simply take some source-code, +even though it's published publicly. This code may be available to be seen by anyone, but that does not mean it's also +available to be used by anyone in other projects. + +Another important note to keep in mind is that even if some project has an open-source license, that license may have +conditions which are incompatible with our codebase (such as requiring all of the code that links to this new part to +also be licensed under the same license, which our code-base is not currently under). That is why it's necessary to +understand a license before using code available under it. Simple attribution often isn't everything that the license +requires. + +??? tip "Learn more about software licensing" + + If you are new to software licensing, you can check out [this](https://itsdrike.com/posts/software-licenses/) + article, which does a good job at explaining the basics. + +## Generally inappropriate behavior + +Outside of just harassment and plagiarism, there are countless other behaviors which we consider unacceptable, as they +may be offensive, and discourage people from engaging with our community. + +**Examples of generally inappropriate behavior:** + +- The use of sexualized language or imagery of any kind +- The use of inappropriate images, including in an account's avatar +- The use of inappropriate language, including in an account's nickname +- Any spamming, flamming, baiting or other attention-stealing behavior +- Discussing topics that are overly polarizing, sensitive, or incite arguments. +- Responding with "RTFM", "just google it" or similar response to help requests +- Other conduct which could be reasonably considered inappropriate + +**Examples of generally appropriate behavior:** + +- Being kind and courteous to others +- Collaborating with other community members +- Gracefully accepting constructive criticism +- Using welcoming and inclusive language +- Showing empathy towards other community members + +## Scope + +This Code of Conduct applies within all community spaces, including this repository itself, conversations on any +platforms officially connected to this project (such as in GitHub issues, emails or platforms like discord). It also +applies when an individual is officially representing the community in public spaces. Examples of representing our +community include using an official social media account, or acting as an appointed representative at an online or +offline event. + +All members involved with the project are expected to follow this Code of Conduct, no matter their position in the +project's hierarchy, this Code of Conduct applies equally to contributors, maintainers and people seeking +help/reporting bugs, etc. + +## Enforcement Responsibilities + +Whenever a participant has made a mistake, we expect them to take responsibility for their actions. If someone has been +harmed or offended, it is our responsibility to listen carefully and respectfully, and to do our best to right the +wrong. + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, +offensive, harmful, or otherwise undesirable. + +Community leaders have the right and responsibility to remove, edit or reject comments, commits, code, wiki edits, +issues and other contributions within the enforcement scope that are not aligned to this Code of Conduct, and will +communicate reasons for moderation decisions when appropriate. + +If you have experienced or witnessed unacceptable behavior constituting a code of conduct violation or have any other +code of conduct concerns, please let us know and we will do our best to resolve this issue. + +## Reporting a Code of Conduct violation + +If you think that someone is violating the Code of Conduct, you can report it to any repository maintainer, either by +email or through a Discord DM. You should avoid using public channels for reporting these violations, and instead do so +in private discussion with a maintainer. + +## Sources + +The open-source community has an incredible amount of resources that people have freely provided to others and we all +depend on these projects in many ways. This code of conduct article is no exception and there were many open source +projects that has helped bring this code of conduct to existence. For that reason, we'd like to thank all of these +communities and projects for keeping their content open and available to everyone, but most notably we'd like to thank +the projects with established codes of conduct and diversity statements that we used as our inspiration. Below is the +list these projects: + +- Python: +- Contributor Covenant: +- Rust-lang: +- Code Fellows: +- Python Discord: + +## License + +All content of this page is licensed under a Creative Commons Attributions license. + +For more information about this license, see: diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 00000000..971e2a34 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,22 @@ +# License + +This project is licensed under the **GNU Lesser General Public License** (LGPL) version 3. + +The LGPL license allows you to use mcproto as a library pretty much in any code-base, including in proprietary +code-bases. However, if you wish to make a derivative project to mcproto itself, such a project will need to be licensed under +LGPL as well. + +??? example "Full LICENSE text" + + ```title="LICENSE.txt" + --8<-- "LICENSE.txt" + ``` + +Some parts of the project follow a different license. See the `LICENSE-THIRD-PARTY.txt` file, which lists all of these +parts and their respective licenses + +??? example "Full LICENSE-THIRD-PARTY text" + + ```title="LICENSE-THIRD-PARTY.txt" + --8<-- "LICENSE-THIRD-PARTY.txt" + ``` diff --git a/mkdocs.yml b/mkdocs.yml index 6a4bacec..2e7ac38d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,9 @@ nav: - Installation: installation.md - Version Guarantees: version_guarantees.md - Changelog: changelog.md + - Community: + - Code of Conduct: code_of_conduct.md + - License: license.md theme: name: material From 2863450694df29534fb8b3dc38434c16f0b75778 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 13:12:14 +0200 Subject: [PATCH 07/85] Include changelog.md --- docs/changelog.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 0789f663..5e3834e4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ +# Changelog + !!! danger "" Major and minor releases also include the changes specified in prior development releases. -TODO: Include changelog + find a way to have towncrier generate unreleased changes +TODO: Find a way to have towncrier generate unreleased changes + +--8<-- "CHANGELOG.md" From fe8541a183c890d132d23701ee9768f5fa50cc6e Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 13:15:27 +0200 Subject: [PATCH 08/85] Add attribution page to docs --- ATTRIBUTION.md | 2 +- docs/attribution.md | 3 +++ mkdocs.yml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 docs/attribution.md diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 3aaeb5eb..45145236 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -1,4 +1,4 @@ -This file serves as a way to explicitly give credit to projects which made mcproto possible. +This document serves as a way to explicitly give credit to projects which made mcproto possible. Note that as with any other project, if there was some code that was directly utilized from these projects, it will be mentioned in `LICENSE-THIRD-PARTY.txt`, not in here. This file isn't meant to serve as a place to disclose used code diff --git a/docs/attribution.md b/docs/attribution.md new file mode 100644 index 00000000..91564f74 --- /dev/null +++ b/docs/attribution.md @@ -0,0 +1,3 @@ +# Attribution + +--8<-- "ATTRIBUTION.md" diff --git a/mkdocs.yml b/mkdocs.yml index 2e7ac38d..8b566bd1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,7 @@ nav: - Changelog: changelog.md - Community: - Code of Conduct: code_of_conduct.md + - Attributions: attribution.md - License: license.md theme: From 1046a92363cbeb4d651a85328f8524ad75ca4e69 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 14:55:46 +0200 Subject: [PATCH 09/85] Add bug reporting instructions --- .pre-commit-config.yaml | 3 + docs/contributing/making_a_pr.md | 1 + docs/contributing/reporting_a_bug.md | 98 ++++++++++++++++++++++++++++ mkdocs.yml | 7 ++ 4 files changed, 109 insertions(+) create mode 100644 docs/contributing/making_a_pr.md create mode 100644 docs/contributing/reporting_a_bug.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 340ed502..eafa009a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,9 @@ repos: - id: check-merge-conflict - id: check-toml # For pyproject.toml - id: check-yaml # For workflows + # Only parse the files for syntax, don't do full load. + # We need this because of mkdocs.yml, which uses some custom tags to perform dynamic imports from python. + args: ["--unsafe"] - id: end-of-file-fixer - id: trailing-whitespace args: [--markdown-linebreak-ext=md] diff --git a/docs/contributing/making_a_pr.md b/docs/contributing/making_a_pr.md new file mode 100644 index 00000000..f7aa7700 --- /dev/null +++ b/docs/contributing/making_a_pr.md @@ -0,0 +1 @@ +TODO: Add this diff --git a/docs/contributing/reporting_a_bug.md b/docs/contributing/reporting_a_bug.md new file mode 100644 index 00000000..e67034d1 --- /dev/null +++ b/docs/contributing/reporting_a_bug.md @@ -0,0 +1,98 @@ +# Bug reports + +Mcproto is an actively maintained project that we constantly strive to improve. With a project of this siez and +complexity, bugs may occur. If you think you have discovered a bug, you can help us by submitting an issue to our +public [issue tracker](https://github.com/py-mine/mcproto/issues), following this guide. + +## Before creating an issue + +Before opening a new issue with your bug report, please do the following things: + +### Upgrade to latest version + +Chances are that the bug you discovered was already fixed in a subsequent version. Thus, before reporting an issue, +ensure that you're running the [latest version](../changelog.md) of mcproto. + +!!! warning "Bug fixes are not backported" + + Please understand that only bugs that occur in the latest version of mcproto will be addressed. Also, to reduce + duplicate efforts, fixes cannot be backported to earlier versions. + +### Search for existing issues + +It's possible that the issue you're having was already reported. Please take some time and search the existing issues +in the GitHub repository for your problem. If you do find an existing issue that matches the problem you're having, +simply leave a :thumbsup: reaction instead (avoid commenting "I have this issue too" or similar, as that ultimately +just clutters the discussion in that issue, but if you do think that you have something meaningful to add, please do). + +!!! note + + Make sure to also check the closed issues. By default, github issue search will start with: `is:issue is:open`, + remove the `is:open` part to search all issues, not just the opened ones. It's possible that we seen this issue + before, but closed the issue as something that we're unable to fix. + +## Creating a new issue + +At this point, when you still haven't found a solution to your problem, we encourage you to create an issue. +To do so, you can click [here][open-bug-issue]. + +[open-bug-issue]: https://github.com/py-mine/mcproto/issues/new?labels=type%3A+bug&template=bug_report.yml + +## Writing good bug reports + +We have a GitHub issue template set up, which will guide you towards telling us everything that we need to know. +However, for the best results, keep reading through this section. In here, we'll explain how a well formatted issue +should look like in general and what it should contain. + +### Issue Title + +A good title is short and descriptive. It should be a one-sentence executive summary of the issue, so the impact and +severity of the bug you want to report can be inferred right from the title. + +| | Example | +| ---------------------------------------------------------- | -------------------------------------------------------------------- | +| :material-check:{ style="color: #4DB6AC" } **Clear** | Ping packet has incorrect ID | +| :material-close:{ style="color: #EF5350" } **Wordy** | The Ping packet has an incorrect packet ID of 0, when it should be 1 | +| :material-close:{ style="color: #EF5350" } **Unclear** | Ping packet is incorrect | +| :material-close:{ style="color: #EF5350" } **Non-english** | El paquete ping tiene una identificación incorrecta | +| :material-close:{ style="color: #EF5350" } **Useless** | Help | + +### Bug description + +Now, to the bug you want to report. Provide a clear, focused, specific and concise summary of the bug you encountered. +Explain why you think this is a bug that should be reported to us. Adhere to the following principles: + +1. **Explain the what, not the how** – don't explain [how to reproduce the bug](#reproduction) here, + we're getting there. Focus on articulating the problem and its impact as clearly as possible. +2. **Keep it short and concise** - if the bug can be precisely explained in one or two sentences, perfect. Don't + inflate it - maintainers and future users will be grateful for having to read less. +3. **Don't under-explain** - don't leave out important details just to keep things short. While keeping things short is + important, if something is relevant, mention it. It is more important for us to have enough information to be able + to understand the bug, even if it means slightly longer bug report. +4. **One bug at a time** - if you encounter several unrelated bugs, please create separate issues for them. Don't + report them in the same issue, as this makes it difficult for others when they're searching for existing issues and + also for us, since we can't mark such an issue as complete if only one of the bugs was fixed. + +--- + +:material-run-fast: **Stretch goal** – if you have a link to an existing page that describes the issue, or otherwise +explains some of your claims, include it. Usually, this will be a link leading to the Minecraft +protocol documentation for something. + +:material-run-fast: **Stretch goal \#2** – if you found a workaround or a way to fix +the bug, you can help other users temporarily mitigate the problem before +we maintainers can fix the bug in our code base. + +### Reproduction + +A minimal reproducible example is at the heart of every well-written bug report, as it allows us maintainers to +instantly recreate the necessary conditions to inspect the bug and quickly find its root cause from there. It's a +proven fact that issues with concise and small reproductions can be fixed much faster. + +Focus on creating a simple and small code snippet that we can run to see the bug. Do your best to avoid giving us large +snippets or whole files just for the purpose of the reproducible example, do your best to reduce the amount of code as +much as you can and try to avoid using external dependencies in the snippet (except for mcproto of course). + +Sometimes, the bug can't be described in terms of code snippets, such as when reporting a mistake in the documentation, +in that case, provide a link to the documentation or whatever other relevant that will allows us to see the bug with +minimal effort. diff --git a/mkdocs.yml b/mkdocs.yml index 8b566bd1..33a12321 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,6 +14,10 @@ nav: - Code of Conduct: code_of_conduct.md - Attributions: attribution.md - License: license.md + - Contributing: + - Reporting a bug: contributing/reporting_a_bug.md + - Asking a question: https://github.com/py-mine/mcproto/discussions + - Making a pull request: contributing/making_a_pr.md theme: name: material @@ -70,6 +74,9 @@ markdown_extensions: - pymdownx.superfences - pymdownx.snippets - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.tabbed: alternate_style: true From 47cc0b32201c110adaee8e02391f6daab9d92482 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 15:09:23 +0200 Subject: [PATCH 10/85] Use new link to code of conduct docs --- CODE-OF-CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md index 53ba588f..6af12937 100644 --- a/CODE-OF-CONDUCT.md +++ b/CODE-OF-CONDUCT.md @@ -1,2 +1,2 @@ You can find our Code of Conduct in the project's documentation -[here](https://mcproto.readthedocs.io/en/latest/pages/code-of-conduct/) +[here](https://py-mine.github.io/en/mcproto/latest/code_of_conduct/) From d706f14319a05cb1516a3ad1d7373208fd42abeb Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 21:54:12 +0200 Subject: [PATCH 11/85] Add next steps section to reporting_a_bug page --- docs/contributing/reporting_a_bug.md | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/contributing/reporting_a_bug.md b/docs/contributing/reporting_a_bug.md index e67034d1..7be27523 100644 --- a/docs/contributing/reporting_a_bug.md +++ b/docs/contributing/reporting_a_bug.md @@ -96,3 +96,52 @@ much as you can and try to avoid using external dependencies in the snippet (exc Sometimes, the bug can't be described in terms of code snippets, such as when reporting a mistake in the documentation, in that case, provide a link to the documentation or whatever other relevant that will allows us to see the bug with minimal effort. + +## Next steps + +Once you submit the issue, the main part of reporting a bug is done, but things aren't completely over yet. You now +have 2 choices: + +### Wait for us to get to the problem + +If you don't wish to solve the bug yourself, all that remains is waiting for us to handle it. + +Please understand that we are all volunteers here and we work on the project simply the fun of it. This means that we +may sometimes have other priorities in life or we just want to work on some more interesting tasks first. It might +therefore take a while for us to get to your bug (don't worry though, in most cases, we're pretty quick). Even if +things are slower, we kindly ask you to avoid posting comments like "Any progress on this?" as they are not helpful and +create unnecessary clutter in the discussion. + +When we do address your issue, we might need further information from you. GitHub has a notification system, so once we +respond, you will be notified there. Note that, by default, these notifications might not be forwarded to your email or +elsewhere, so please check GitHub periodically for updates. + +Finally, when we fix your bug, we will mark the issue as closed (GitHub will notify you of this too). Once that +happens, your bug should be fixed, but we appreciate it if you take the time to verify that everything is working +correctly. If the issue persists, you can reopen the issue and let us know. + +!!! warning "Issues are fixed on the main branch" + + Do note that when we close an issue, it means that we have fixed your bug in the `main` branch of the repository. + That doesn't necessarily mean the fix has been released on PyPI yet, so you might still need to wait for the + next release. Alternatively, you can also try the [git installation](../installation.md#latest-git-version) to + get the project right from that latest `main` branch. + +### Attempt to solve it yourself + +!!! quote + + The fastest way to get something done is to avoid waiting on others. + +If you wish to try and tackle the bug yourself, let us know by commenting on the issue with something like "I'd like to +work on this". This helps us avoid duplicate efforts and ensures that we don't work on something you're already +addressing. + +Once a maintainer sees your comment, they will assign the issue to you. Being assigned is a soft approval from us, +giving you the green light to start working. + +Of course, you are welcome to start working on the issue even before being officially assigned. However, please be +aware that sometimes we choose not to fix certain bugs for specific reasons. In such cases, your work might not end up +being used. + +Before starting your work though, make sure to also read our [pull request guide](./making_a_pr.md). From 6e40d5c0ed56977b1e27716fba58570ca48e3612 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 22:59:14 +0200 Subject: [PATCH 12/85] Add making_a_pr.md & template for contributing guides --- docs/assets/draft-pr-conversion.png | Bin 0 -> 21722 bytes docs/assets/draft-pr-creation.png | Bin 0 -> 21367 bytes docs/assets/draft-pr-unmark.png | Bin 0 -> 114061 bytes docs/contributing/guides/changelog.md | 0 docs/contributing/guides/deprecations.md | 0 docs/contributing/guides/great_commits.md | 0 docs/contributing/guides/index.md | 1 + docs/contributing/guides/installation.md | 0 docs/contributing/guides/precommit.md | 0 docs/contributing/guides/style_guide.md | 0 docs/contributing/guides/type_hints.md | 0 docs/contributing/guides/unit_tests.md | 0 docs/contributing/making_a_pr.md | 54 +++++++++++++++++++++- mkdocs.yml | 10 ++++ 14 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 docs/assets/draft-pr-conversion.png create mode 100644 docs/assets/draft-pr-creation.png create mode 100644 docs/assets/draft-pr-unmark.png create mode 100644 docs/contributing/guides/changelog.md create mode 100644 docs/contributing/guides/deprecations.md create mode 100644 docs/contributing/guides/great_commits.md create mode 100644 docs/contributing/guides/index.md create mode 100644 docs/contributing/guides/installation.md create mode 100644 docs/contributing/guides/precommit.md create mode 100644 docs/contributing/guides/style_guide.md create mode 100644 docs/contributing/guides/type_hints.md create mode 100644 docs/contributing/guides/unit_tests.md diff --git a/docs/assets/draft-pr-conversion.png b/docs/assets/draft-pr-conversion.png new file mode 100644 index 0000000000000000000000000000000000000000..1467615a343407e65a613b1b63ba8f147fa5d820 GIT binary patch literal 21722 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 literal 0 HcmV?d00001 diff --git a/docs/assets/draft-pr-creation.png b/docs/assets/draft-pr-creation.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e2fc865cb613a92bec7a1456156a68e725438d GIT binary patch literal 21367 zcmafbb95x#7j4JJ#J25BoJ?#@Y#S5XnIse2wrywPj%`eATd%+0Tkr4JYc*=sy|?O| zt9|x9)e%Yxl1T7)@Bjb+Nm@!w1polk0sUTq1qXeOe=d7~zTh0Cw44C|mV9E5z z_y7P2Kw3=Xr$^RBx3|a7#cuA;RrW^e)G+}n)I>uGBjdyc*nC|ERa7zKO2gXT76anaF#-ky+X{JLkdTnfApJFD7T$s0YpGiQZHfXHiGqI~3ad+4 zP*@0a>o#RxdQn#W=uv&D_Hst{;QaD+=@qOq zTiJmFSN;;~X-p``g|FjFJ7np4Q{*-3eyVQDT%*wt20S%%{mkCpL4u;e!u)O-O7c0z z@c1}iC266i3FYwzq&?;L&8eBenRUrc1r12hSrS2ktrIuoM>+_g?HR;VEVqHOajS&G|k(-Rjt16rryjh?G%Om!DT(z`M#yeClbzCNg#kNC(34 zzTND^oRfDcAiDqLBlkvp0ZWa?#2$A6^c5g1*vNrT* z0nb{!Ok+IXU(=ZZfc*JwU)rw^>_bd%CfNnd>W#FYb?@<|!_;C055T9GRwkU~??=(i zi)me-4_d^%ycXQvWVjB5-q*nLv+YU&SjLr!|j$Au*JV*Aze&F`;_yGVl?-zysul^V~tP0o?b&2PB17a7flU*pMd zt)Fey<282mHhx_nEPxQfy3=J@CH|`~zRzY>*Z#v&^~EmMT21ef(J`JB&#MW(MKF_r zs>#T$jONFq^~~C^I$$;beW02mqhY0!WR$_@!f#aDGv|Tllh7-{t@nwnWn^xND1)2( z-v^Vb{;KIM*JGu1&ZkM^TwPzl}6BlKOHjV-TOB5mPB-?S6VJK6Y^ReLUoUy>hsn4r&6vyZXG{m=BI< z&+$h`8)3-?Br^-XrJ|p0`JFs3+vi=pudnxxr)LliVt737te5fv`Znu)yneVZq&TRCx9V0qQA zXZpA{LIuZ$vrD2nFZ9b}$RwsASDYEW-8ktvSnw_Q5K2#W%aOdav{=&apUZ&}3k$0_ zU>qZ{%$m*oeC5sgB_+6|u&m>2vCH z9sH|sOiwPQ+?ZXP=?>i_tWH(_FiEse9qGN#g!sIYSQwJralqf!Z@-UM^QLa`d%am} z_UH2xpzkrS`{l=I(lV-B- z%?tM-;UnPp*qL( z*Oz4K%~hQAI8#U*;y>Zt0+4EefF*TGwz=tLvVr#}Tf43m{r8FS#kG-zI{%0Dx8*^> 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 KMNUMnLSTZO$6p=* literal 0 HcmV?d00001 diff --git a/docs/assets/draft-pr-unmark.png b/docs/assets/draft-pr-unmark.png new file mode 100644 index 0000000000000000000000000000000000000000..0b9382b90e3fcc423e2d574292efc89c6c0ef7d8 GIT binary patch literal 114061 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 Date: Fri, 2 Aug 2024 23:13:31 +0200 Subject: [PATCH 13/85] Multiple minor fixes in the version guarantees page --- docs/version_guarantees.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/version_guarantees.md b/docs/version_guarantees.md index 8a64b7c9..9f1a03e5 100644 --- a/docs/version_guarantees.md +++ b/docs/version_guarantees.md @@ -12,7 +12,8 @@ it can be hard to discern what can be considered a breaking change, and what isn First thing to keep in mind is that breaking changes only apply to **publicly documented functions and classes**. If it's not listed in the documentation here, it's an internal feature, that isn't considered a part of the public API, -and thus is bound to change. This includes documented attributes that start with an underscore. +and thus is bound to change. This includes documented attributes that start with an underscore and documented API +that is explicitly marked as internal. !!! note @@ -23,7 +24,7 @@ and thus is bound to change. This includes documented attributes that start with - Changing the default parameter value of a function to something else. - Renaming (or removing) a function without an alias to the old function. - Adding or removing parameters of a function. -- Removing deprecated alias to a renamed function +- Removing deprecated alias to a renamed function. ## Examples of Non-Breaking Changes @@ -31,7 +32,7 @@ and thus is bound to change. This includes documented attributes that start with - Renaming (or removing) private underscored attributes. - Adding an element into `__slots__` of a data class. - Changing the behavior of a function to fix a bug. -- Changes in the typing behavior of the library. +- Changes in the typing definitions of the public API. - Changes in the documentation. - Modifying the internal protocol connection handling. - Updating the dependencies to a newer version, major or otherwise. From 1a323203cb68af3c9cd9dd89dd04071b33531dfd Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 23:23:13 +0200 Subject: [PATCH 14/85] Add work-in-progress notices --- docs/changelog.md | 11 ++++++++++- docs/contributing/guides/changelog.md | 3 +++ docs/contributing/guides/deprecations.md | 3 +++ docs/contributing/guides/great_commits.md | 3 +++ docs/contributing/guides/index.md | 8 ++++++++ docs/contributing/guides/installation.md | 3 +++ docs/contributing/guides/precommit.md | 3 +++ docs/contributing/guides/style_guide.md | 3 +++ docs/contributing/guides/type_hints.md | 3 +++ docs/contributing/guides/unit_tests.md | 3 +++ 10 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 5e3834e4..f70d8fe0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,15 @@ Major and minor releases also include the changes specified in prior development releases. -TODO: Find a way to have towncrier generate unreleased changes +!!! bug "Missing unreleased changes" + + This changelog doesn't contain any unreleased (pending) changes, even if they are present in this version of the + project documentation already. If you are interested in knowing what these changes are, you can take a look at the + [`changes/`](https://github.com/py-mine/mcproto/tree/main/changes) directory of the project. + + In the future, we want to display these changes properly in this documentation, however, doing so requires running + a command dynamically, and showing it's output here, which we currently don't support. + + --8<-- "CHANGELOG.md" diff --git a/docs/contributing/guides/changelog.md b/docs/contributing/guides/changelog.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/changelog.md +++ b/docs/contributing/guides/changelog.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/deprecations.md b/docs/contributing/guides/deprecations.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/deprecations.md +++ b/docs/contributing/guides/deprecations.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/great_commits.md b/docs/contributing/guides/great_commits.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/great_commits.md +++ b/docs/contributing/guides/great_commits.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/index.md b/docs/contributing/guides/index.md index a2503b0f..a43bab20 100644 --- a/docs/contributing/guides/index.md +++ b/docs/contributing/guides/index.md @@ -1 +1,9 @@ # Contributing guides & guidelines + +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. + + + +--8<-- "CONTRIBUTING.md" diff --git a/docs/contributing/guides/installation.md b/docs/contributing/guides/installation.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/installation.md +++ b/docs/contributing/guides/installation.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/precommit.md b/docs/contributing/guides/precommit.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/precommit.md +++ b/docs/contributing/guides/precommit.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/style_guide.md b/docs/contributing/guides/style_guide.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/style_guide.md +++ b/docs/contributing/guides/style_guide.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/type_hints.md b/docs/contributing/guides/type_hints.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/type_hints.md +++ b/docs/contributing/guides/type_hints.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/unit_tests.md b/docs/contributing/guides/unit_tests.md index e69de29b..fb61abca 100644 --- a/docs/contributing/guides/unit_tests.md +++ b/docs/contributing/guides/unit_tests.md @@ -0,0 +1,3 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. From 835f3db95b52f7c8a092872e69d7343e13fbb38e Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 2 Aug 2024 23:53:18 +0200 Subject: [PATCH 15/85] Fix docs url for code-of-conduct --- CODE-OF-CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md index 6af12937..39b40e06 100644 --- a/CODE-OF-CONDUCT.md +++ b/CODE-OF-CONDUCT.md @@ -1,2 +1,2 @@ You can find our Code of Conduct in the project's documentation -[here](https://py-mine.github.io/en/mcproto/latest/code_of_conduct/) +[here](https://py-mine.github.io/mcproto/latest/code_of_conduct/) From ffba25ecd014c7b458888d8079c5f5a3ff4a40f0 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 11:14:16 +0200 Subject: [PATCH 16/85] Use dashes instead of underscores in file names --- docs/{code_of_conduct.md => code-of-conduct.md} | 0 .../{great_commits.md => great-commits.md} | 0 .../guides/{style_guide.md => style-guide.md} | 0 .../guides/{type_hints.md => type-hints.md} | 0 .../guides/{unit_tests.md => unit-tests.md} | 0 .../{making_a_pr.md => making-a-pr.md} | 2 +- .../{reporting_a_bug.md => reporting-a-bug.md} | 2 +- ...rsion_guarantees.md => version-guarantees.md} | 0 mkdocs.yml | 16 ++++++++-------- 9 files changed, 10 insertions(+), 10 deletions(-) rename docs/{code_of_conduct.md => code-of-conduct.md} (100%) rename docs/contributing/guides/{great_commits.md => great-commits.md} (100%) rename docs/contributing/guides/{style_guide.md => style-guide.md} (100%) rename docs/contributing/guides/{type_hints.md => type-hints.md} (100%) rename docs/contributing/guides/{unit_tests.md => unit-tests.md} (100%) rename docs/contributing/{making_a_pr.md => making-a-pr.md} (97%) rename docs/contributing/{reporting_a_bug.md => reporting-a-bug.md} (99%) rename docs/{version_guarantees.md => version-guarantees.md} (100%) diff --git a/docs/code_of_conduct.md b/docs/code-of-conduct.md similarity index 100% rename from docs/code_of_conduct.md rename to docs/code-of-conduct.md diff --git a/docs/contributing/guides/great_commits.md b/docs/contributing/guides/great-commits.md similarity index 100% rename from docs/contributing/guides/great_commits.md rename to docs/contributing/guides/great-commits.md diff --git a/docs/contributing/guides/style_guide.md b/docs/contributing/guides/style-guide.md similarity index 100% rename from docs/contributing/guides/style_guide.md rename to docs/contributing/guides/style-guide.md diff --git a/docs/contributing/guides/type_hints.md b/docs/contributing/guides/type-hints.md similarity index 100% rename from docs/contributing/guides/type_hints.md rename to docs/contributing/guides/type-hints.md diff --git a/docs/contributing/guides/unit_tests.md b/docs/contributing/guides/unit-tests.md similarity index 100% rename from docs/contributing/guides/unit_tests.md rename to docs/contributing/guides/unit-tests.md diff --git a/docs/contributing/making_a_pr.md b/docs/contributing/making-a-pr.md similarity index 97% rename from docs/contributing/making_a_pr.md rename to docs/contributing/making-a-pr.md index bc7c2f74..719ec5fb 100644 --- a/docs/contributing/making_a_pr.md +++ b/docs/contributing/making-a-pr.md @@ -21,7 +21,7 @@ issues. If you find anything interesting there that you'd wish to work on, leave like: "I'd like to work on this". Even if you do have an idea already, we heavily recommend (though not require) that you first make an issue, this can -be a [bug report](./reporting_a_bug.md), but also a feature request, or something else. Once you made the issue, leave +be a [bug report](./reporting-a-bug.md), but also a feature request, or something else. Once you made the issue, leave a: "I'd like to work on this" comment on it. Eventually, a maintainer will get back to you and you will be assigned to the issue. By getting assigned, you reserve diff --git a/docs/contributing/reporting_a_bug.md b/docs/contributing/reporting-a-bug.md similarity index 99% rename from docs/contributing/reporting_a_bug.md rename to docs/contributing/reporting-a-bug.md index 7be27523..541b5131 100644 --- a/docs/contributing/reporting_a_bug.md +++ b/docs/contributing/reporting-a-bug.md @@ -144,4 +144,4 @@ Of course, you are welcome to start working on the issue even before being offic aware that sometimes we choose not to fix certain bugs for specific reasons. In such cases, your work might not end up being used. -Before starting your work though, make sure to also read our [pull request guide](./making_a_pr.md). +Before starting your work though, make sure to also read our [pull request guide](./making-a-pr.md). diff --git a/docs/version_guarantees.md b/docs/version-guarantees.md similarity index 100% rename from docs/version_guarantees.md rename to docs/version-guarantees.md diff --git a/mkdocs.yml b/mkdocs.yml index fbe60777..020cedec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,25 +8,25 @@ nav: - Home: index.md - Installation: - Installation: installation.md - - Version Guarantees: version_guarantees.md + - Version Guarantees: version-guarantees.md - Changelog: changelog.md - Community: - - Code of Conduct: code_of_conduct.md + - Code of Conduct: code-of-conduct.md - Attributions: attribution.md - License: license.md - Contributing: - - Reporting a bug: contributing/reporting_a_bug.md + - Reporting a bug: contributing/reporting-a-bug.md - Asking a question: https://github.com/py-mine/mcproto/discussions - - Making a pull request: contributing/making_a_pr.md + - Making a pull request: contributing/making-a-pr.md - Guides: - contributing/guides/index.md - Installation: contributing/guides/installation.md - - Style Guide: contributing/guides/style_guide.md - - Type hinting: contributing/guides/type_hints.md + - Style Guide: contributing/guides/style-guide.md + - Type hinting: contributing/guides/type-hints.md - Pre-commit: contributing/guides/precommit.md - - Great commits: contributing/guides/great_commits.md + - Great commits: contributing/guides/great-commits.md - Changelog: contributing/guides/changelog.md - - Unit Tests: contributing/guides/unit_tests.md + - Unit Tests: contributing/guides/unit-tests.md - Deprecations: contributing/guides/deprecations.md theme: From 363c907f55e240b14441b99c4a88ee4dff9bfc07 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 11:16:25 +0200 Subject: [PATCH 17/85] Improve directory structure of docs/ --- docs/{ => community}/attribution.md | 0 docs/{ => community}/code-of-conduct.md | 0 docs/{ => community}/license.md | 0 docs/contributing/reporting-a-bug.md | 6 +++--- docs/{ => installation}/changelog.md | 0 docs/{installation.md => installation/index.md} | 0 docs/{ => installation}/version-guarantees.md | 0 mkdocs.yml | 12 ++++++------ 8 files changed, 9 insertions(+), 9 deletions(-) rename docs/{ => community}/attribution.md (100%) rename docs/{ => community}/code-of-conduct.md (100%) rename docs/{ => community}/license.md (100%) rename docs/{ => installation}/changelog.md (100%) rename docs/{installation.md => installation/index.md} (100%) rename docs/{ => installation}/version-guarantees.md (100%) diff --git a/docs/attribution.md b/docs/community/attribution.md similarity index 100% rename from docs/attribution.md rename to docs/community/attribution.md diff --git a/docs/code-of-conduct.md b/docs/community/code-of-conduct.md similarity index 100% rename from docs/code-of-conduct.md rename to docs/community/code-of-conduct.md diff --git a/docs/license.md b/docs/community/license.md similarity index 100% rename from docs/license.md rename to docs/community/license.md diff --git a/docs/contributing/reporting-a-bug.md b/docs/contributing/reporting-a-bug.md index 541b5131..b7c6c1fb 100644 --- a/docs/contributing/reporting-a-bug.md +++ b/docs/contributing/reporting-a-bug.md @@ -11,7 +11,7 @@ Before opening a new issue with your bug report, please do the following things: ### Upgrade to latest version Chances are that the bug you discovered was already fixed in a subsequent version. Thus, before reporting an issue, -ensure that you're running the [latest version](../changelog.md) of mcproto. +ensure that you're running the [latest version](../installation/changelog.md) of mcproto. !!! warning "Bug fixes are not backported" @@ -123,8 +123,8 @@ correctly. If the issue persists, you can reopen the issue and let us know. !!! warning "Issues are fixed on the main branch" Do note that when we close an issue, it means that we have fixed your bug in the `main` branch of the repository. - That doesn't necessarily mean the fix has been released on PyPI yet, so you might still need to wait for the - next release. Alternatively, you can also try the [git installation](../installation.md#latest-git-version) to + That doesn't necessarily mean the fix has been released on PyPI yet, so you might still need to wait for the next + release. Alternatively, you can also try the [git installation](../installation/index.md#latest-git-version) to get the project right from that latest `main` branch. ### Attempt to solve it yourself diff --git a/docs/changelog.md b/docs/installation/changelog.md similarity index 100% rename from docs/changelog.md rename to docs/installation/changelog.md diff --git a/docs/installation.md b/docs/installation/index.md similarity index 100% rename from docs/installation.md rename to docs/installation/index.md diff --git a/docs/version-guarantees.md b/docs/installation/version-guarantees.md similarity index 100% rename from docs/version-guarantees.md rename to docs/installation/version-guarantees.md diff --git a/mkdocs.yml b/mkdocs.yml index 020cedec..bb9b18b4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,13 +7,13 @@ repo_name: py-mine/mcproto nav: - Home: index.md - Installation: - - Installation: installation.md - - Version Guarantees: version-guarantees.md - - Changelog: changelog.md + - Installation: installation/index.md + - Version Guarantees: installation/version-guarantees.md + - Changelog: installation/changelog.md - Community: - - Code of Conduct: code-of-conduct.md - - Attributions: attribution.md - - License: license.md + - Code of Conduct: community/code-of-conduct.md + - Attributions: community/attribution.md + - License: community/license.md - Contributing: - Reporting a bug: contributing/reporting-a-bug.md - Asking a question: https://github.com/py-mine/mcproto/discussions From b692426c3d88af74d09910bcd8290cccd44d344f Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 13:28:23 +0200 Subject: [PATCH 18/85] Add contributing guidelines index page --- docs/contributing/guides/index.md | 49 ++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/contributing/guides/index.md b/docs/contributing/guides/index.md index a43bab20..2a757dcd 100644 --- a/docs/contributing/guides/index.md +++ b/docs/contributing/guides/index.md @@ -1,9 +1,50 @@ # Contributing guides & guidelines -!!! bug "Work In Progress" +Welcome to the contributing guides & guidelines for mcproto. This documentation is intended for our contributors, +interested in writing or modifying mcproto itself. If you just wish to use mcproto in your project, you can safely skip +this section. - This page is still being written. The content below (if any) may change. +Mcproto is a relatively large project and maintaining it is no easy task. With a project like that, consistency and +good code quality becomes very important to keep the code-base readable and bug-free. To achieve this, we have put +together these guidelines that will explain the code style and coding practices that we expect our contributors to +follow. - +This documentation will also include various guides that tell you how to set up our project for development and explain +the automated tools that we use to improve our coding experience and enforce a bunch of the code style rules quickly +and without the need for human review. ---8<-- "CONTRIBUTING.md" +## Changes to these guidelines + +While we're confident and happy with the current code style and tooling, we acknowledge that change is inevitable. New +tools are constantly being developed, and we have already made significant updates to our code style in the past. + +Every project evolves over time, and these guidelines are no exception. This documentation is open to pull requests and +changes from contributors. Just ensure that any updates to this document are in sync with the codebase. If you propose +a code style change, you must apply that change throughout the codebase to maintain internal consistency. + +If you believe you have something valuable to add or change, please submit a pull request. For major style changes, we +strongly encourage you to open an issue first, as we may not always agree with significant alterations. For minor +clarity improvements or typo fixes, opening an issue isn't necessary. + +We tried to design our specifications to be straightforward and comprehensive, but we might not always succeed, as +we're doing so from our perspective of already having extensive background knowledge. Therefore, we welcome any clarity +improvements to the documentation. If you think you can explain something better, please contribute. + +## Footnotes + +We understand that going through all of these guidelines can be time-consuming and a lot to remember. However, we +strongly encourage you to review them, especially if you haven't worked with these tools or followed such best +practices before. + +Every page in this contributing guides category has an abstract at the top, summarizing its content. This allows you to +quickly determine if you are already familiar with the topic or, if you're re-reading, to quickly recall what the page +covers. + +We believe these guides will be beneficial to you beyond our codebase, as they promote good coding practices and help +make your code cleaner. You will likely be able to apply much of the knowledge you gain here to your own projects. + +## Disclaimer + +These documents were inspired by [Python Discord's CONTRIBUTING agreement.][pydis-contributing] + +[pydis-contributing]: https://github.com/python-discord/bot/blob/master/CONTRIBUTING.md From 676f4355ba4f09000076d2641d9c984c28080f94 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 13:28:44 +0200 Subject: [PATCH 19/85] Add project setup instructions --- docs/contributing/guides/installation.md | 3 - docs/contributing/guides/setup.md | 170 +++++++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 171 insertions(+), 4 deletions(-) delete mode 100644 docs/contributing/guides/installation.md create mode 100644 docs/contributing/guides/setup.md diff --git a/docs/contributing/guides/installation.md b/docs/contributing/guides/installation.md deleted file mode 100644 index fb61abca..00000000 --- a/docs/contributing/guides/installation.md +++ /dev/null @@ -1,3 +0,0 @@ -!!! bug "Work In Progress" - - This page is still being written. The content below (if any) may change. diff --git a/docs/contributing/guides/setup.md b/docs/contributing/guides/setup.md new file mode 100644 index 00000000..95839835 --- /dev/null +++ b/docs/contributing/guides/setup.md @@ -0,0 +1,170 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. + +# Setting up the project + +???+ abstract + + This guide describes the very basics of setting up our project. + + It explains how to use `poetry` to install the python dependencies for the project. After which it goes over using + poetry (activating the virtual environment, keeping the dependencies up to date as we update them, adding / + removing dependencies and poetry dependency group). + +## Pre-requisites + +A basic knowledge of [git and GitHub][git-and-github], alongside working within the terminal and running commands is a +requirement to work on this project. + +[git-and-github]: https://docs.github.com/en/get-started/start-your-journey/about-github-and-git + +## Poetry + +This project uses [`poetry`](https://python-poetry.org/docs/). Poetry is a tool for managing python dependencies in a +reproducible way, ensuring that everyone is using the same versions. It creates virtual environments for each project, +which ensures that your global dependencies won't clash with the project. + +??? question "More about virtual environments" + + A python virtual environment is essentially a separate mini installation of python used purely for the project + you're working on (as opposed to using your system-wide python installation for everything). + + The reason we do this is to avoid dependency conflicts. Consder this: Our project needs library "foo" at version + 2.5.2, however, you also have another unrelated project, that also needs the "foo" library, but this project didn't + yet update this dependency, and requires an older version of this library: 1.2.0. This is a problem, because our + project won't work with a version that old, we're using some of the new features of that library, similarly, your + project won't work with a newer version though. + + With a virtual environment, both projects will have their own isolated python installation, that only contains the + dependencies listed for that project, avoiding any conflicts completely. + + You can create virtual environments manually, with the built-in `venv` python module, but poetry makes this much + simpler. If you want to find out more about virutal environments, check the [official python + documentation][venv-docs]. + +[venv-docs]: https://docs.python.org/3/library/venv.html + +This means you will need to have poetry installed on your system to run our project. To do so, just follow their +[official documentation](https://python-poetry.org/docs/#installation). + +## Dependency installation + +Once installed, you will want to create a new environment for our project, with all of our dependencies installed. Open +a terminal in the project's directory and run the following command: + +```bash +poetry install +``` + +After running this command, the virtual environment will be populated with all of the dependencies that you will need +for running & developing the project. + +## Activating the environment + +The virtual environment that you just created will contain a bunch of executable programs, such as `ruff` (our linter). +One of those executable programs is also `python`, which is the python interpreter for this environment, capable of +using all of those dependencies installed in that environment. + +By default, when you run the `python` command, your machine will use the system-wide python installation though and the +executables present in this environment will not be runnable at all. In order to make your terminal use the programs +from this environment, instead of the global ones, you will need to "activate" the environment. + +Some IDEs/editors are capable of doing this automatically when you open the project, if your editor supports that, you +should configure it to do so. + +??? question "Configuring VSCode to use the poetry environment" + + TODO + +If your IDE doesn't have that option, or you just wish to work from the terminal, you can instead run: + +```bash +poetry shell +``` + +Now you can start the IDE from your terminal, which should make it work within the poetry python environment. + +!!! tip "Execute a single command inside the virtual environment" + + If you just want to urn a single command from the venv, without necessarily having to activate the environment + (often useful in scripts), poetry provides a quick and simple way to do so. All you need to do is prefix any such + command with `poetry run` (e.g. `poetry run ruff`). + +## Keeping your dependencies up to date + +We often update the dependencies of mcproto to keep them up to date. Whenever we make such an update, you will need to +update your virtual environment to prevent it from going out of date. An out of date environment could mean that you're +using older versions of some libraries and what will run on your machine might not match what will run on other +machines with the dependencies updated. + +Thankfully, poetry makes updating the dependencies very easy as all you have to do is re-run the installation command: + +```bash +poetry install +``` + +It can sometimes be hard to know when you need to run the install command, in most cases, even if we did update +something and you're still on an older version, nothing significant will actually happen, however, the moment you start +seeing some errors when you try to run the project, or inconsistencies with the continuous integration workflows from +your local runs, it's a good indication that your dependencies are probably out of date. + +Ideally, you should run this command as often as possible, if there aren't any new changes, it will simply exit +instantly. You should be especially careful when switching git branches, as dependencies might have been changed (most +likely a new dependency was introduced, or an old one got removed), so consider running this command whenever you +switch to another branch, unless you know that branch didn't make any changes to the project dependencies. + +## Poetry dependency groups + +Poetry has a really cool way of splitting up the dependencies that projects need into multiple groups. For example, you +can have a group of dependencies for linting & autoformatting, another group for documentation support, unit-testing, +for releasing the project, etc. + +To see which dependencies belong to which group, you can check the `pyproject.toml` file for the +`[tool.poetry.group.GROUP_NAME.dependencies]` sections. + +By default, `poetry install` will install all non-optional dependency groups. That means all development +dependencies you should need will get installed. + +The reason why we use groups is because in some of our automated workflows, we don't always need all of the project +dependencies and we can save time by only installing the group(s) that we need. It also provides a clean way to quickly +see which dependencies are being used for what. + +The most important group is the `main` group. This group contains all runtime dependencies, which means without these +dependencies, the project wouldn't be runnable at all. It is these libraries that will become the dependencies of our +library when we make a release on PyPI. + +## Installing dependencies + +During the development, you may sometimes want to introduce a new library to the project, to do this, you will first +need to decide which dependency group it should belong to. To do this, identify whether this new dependency will be +required to run the project, or if it's just some tool / utility that's necessary only during the development. + +If it's a runtime dependency, all you need to do is run: + +```bash +poetry add [name-of-your-dependency] +``` + +This will add the dependency to the `main` group. + +However, if you're working with a development dependency, you will want to go over the dependency groups we have (from +`pyproject.toml`) and decide where it should belong. Once you figured that out, you can run: + +```bash +poetry add --group [group-name] [name-of-your-dependency] +``` + +!!! note + + Sometimes, it might make sense to include the same dependency in multiple groups. (Though this is usually quite + rare.) + +## Uninstalling dependencies + +Similarly, we sometimes stop needing a certain dependency. Uninstalling is a very similar process to installation. +First, find which group you want to remove this dependency from and then run: + +```bash +poetry remove --group [group-name] [name-of-your-dependency] +``` diff --git a/mkdocs.yml b/mkdocs.yml index bb9b18b4..6e7dc84a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,7 @@ nav: - Making a pull request: contributing/making-a-pr.md - Guides: - contributing/guides/index.md - - Installation: contributing/guides/installation.md + - Setting things up: contributing/guides/setup.md - Style Guide: contributing/guides/style-guide.md - Type hinting: contributing/guides/type-hints.md - Pre-commit: contributing/guides/precommit.md From ae5ffd0acd393bea112cabf191d693f417f9771a Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 14:31:51 +0200 Subject: [PATCH 20/85] Add note about skipping issues for minor tasks --- docs/contributing/making-a-pr.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/contributing/making-a-pr.md b/docs/contributing/making-a-pr.md index 719ec5fb..8907d1da 100644 --- a/docs/contributing/making-a-pr.md +++ b/docs/contributing/making-a-pr.md @@ -33,6 +33,18 @@ Of course, you are welcome to start working on the issue even before being offic aware that sometimes, we may choose not to pursue a certain feature / bugfix. In such cases, your work might not end up being used, which would be a shame. +!!! note "Minor tasks don't need an issue" + + While we generally do encourage contributors to first create an issue and get assigned to it first. If you're + just fixing a typo, improving the wording, or making some minor optimizations to the code, you can safely skip + this step. + + The point of encouraging issues is to prevent needlessly wasting people's time. However, with these minor tasks, + it might actually take you longer to create a full issue about the problem than it would to just submit a fix. + + There's therefore no point in cluttering the issue tracker with a bunch of small issues that can often be + changed in just a few minutes. + ## Work in Progress PRs Whenever you open a pull request that isn't yet ready to be reviewed and merged, you can mark it as a **draft**. This From 45d4b382bdcb9fc192a4a2a9c365fd660bf1a929 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 14:50:25 +0200 Subject: [PATCH 21/85] Add the golden rules of contributing --- docs/contributing/guides/index.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/contributing/guides/index.md b/docs/contributing/guides/index.md index 2a757dcd..f4258763 100644 --- a/docs/contributing/guides/index.md +++ b/docs/contributing/guides/index.md @@ -13,6 +13,31 @@ This documentation will also include various guides that tell you how to set up the automated tools that we use to improve our coding experience and enforce a bunch of the code style rules quickly and without the need for human review. +## The Golden Rules of Contributing + +These are the general rules which you should follow when contributing. You can glance over these and then go over the +individual guides one by one, or use the references in these rules to get to the specific guide page explaining the +rule. + +!!! note + + This list serves as a quick-reference rather than a full guide. Some of our guidelines aren't directly linked in + these references at all and we heavily encourage you to go over each of the guide pages in the order they're listed + in the docs. + +1. **Lint before you push.** We have multiple code linting rules, which define our general style of the code-base. + These are often enforced through certain tools, which you are expected to run before every push and ideally even + before every commit. The specifics of our linting rules are mentioned in our [style guide](./style-guide.md). + Running all of these tools manually before every commit would however be quite annoying, so we use + [pre-commit](./precommit.md). +2. **Make great commits.** Great commits should be atomic (do one thing only and do it well), with a commit message + that explaining what was done, and why. More on this [here](./great-commits.md). +3. **Make an issue before the PR.** Before you start working on your PR, open an issue and let us know what you're + planning. We described this further in our [making a PR guide](../making-a-pr.md##get-assigned-to-the-issue). +4. **Use assets licensed for public use.** Whenever you're adding a static asset (e.g. images/video files/audio or + even code) that isn't owned/written by you, make sure it has a compatible license with our projects. +5. **Follow our [Code of Conduct](../../community/code-of-conduct.md)** + ## Changes to these guidelines While we're confident and happy with the current code style and tooling, we acknowledge that change is inevitable. New From b5c582aba9bec52f3f0702d56a5f2c66962165ee Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 14:53:21 +0200 Subject: [PATCH 22/85] Note about skipping guide pages --- docs/contributing/guides/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/guides/index.md b/docs/contributing/guides/index.md index f4258763..5d717d18 100644 --- a/docs/contributing/guides/index.md +++ b/docs/contributing/guides/index.md @@ -63,7 +63,7 @@ practices before. Every page in this contributing guides category has an abstract at the top, summarizing its content. This allows you to quickly determine if you are already familiar with the topic or, if you're re-reading, to quickly recall what the page -covers. +covers. Feel free to skip any guide pages if you're already familiar with what they cover. We believe these guides will be beneficial to you beyond our codebase, as they promote good coding practices and help make your code cleaner. You will likely be able to apply much of the knowledge you gain here to your own projects. From 8a9a047cb93f140827a4f08910727fa7c6bb249c Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 15:44:33 +0200 Subject: [PATCH 23/85] Add assumptions setup guide makes --- docs/contributing/guides/setup.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/contributing/guides/setup.md b/docs/contributing/guides/setup.md index 95839835..4b5608ad 100644 --- a/docs/contributing/guides/setup.md +++ b/docs/contributing/guides/setup.md @@ -17,7 +17,13 @@ A basic knowledge of [git and GitHub][git-and-github], alongside working within the terminal and running commands is a requirement to work on this project. +This guide assumes you have already [forked][github-forking] our repository, [clonned it][git-cloning] to your +computer and created your own [git branch][git-branches] to work on. + [git-and-github]: https://docs.github.com/en/get-started/start-your-journey/about-github-and-git +[github-forking]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo +[git-cloning]: https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository +[git-branches]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches ## Poetry From 9cc87bec534183e497d1166a1ef554971fcc7af5 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 15:44:48 +0200 Subject: [PATCH 24/85] Add style guide --- docs/contributing/guides/style-guide.md | 115 ++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/docs/contributing/guides/style-guide.md b/docs/contributing/guides/style-guide.md index fb61abca..3e75fb09 100644 --- a/docs/contributing/guides/style-guide.md +++ b/docs/contributing/guides/style-guide.md @@ -1,3 +1,118 @@ !!! bug "Work In Progress" This page is still being written. The content below (if any) may change. + +# Style Guide + +???+ abstract + + This page describes how we use `ruff` to enforce a consistent code style in our project. + +For clarity and readability, adhering to a consistent code style across the whole project is very important. It is not +unusual that style adjustments will be requested in pull requests. + +It is always a good practice to review the style of the existing code-base before and to adhere to that established +style before adding something new. That applies even if it isn't the code style you generally prefer. (That said, if +you think a code style change of some kind would be justified, feel free to open an issue about it and tell us why.) + +!!! quote + + A style guide is about consistency. Consistency with this style guide is important. Consistency within a project + is more important. Consistency within one module or function is the most important. + + However, know when to be inconsistent -- sometimes style guide recommendations just aren't applicable. When in + doubt, use your best judgment. Look at other examples and decide what looks best. And don't hesitate to ask! + + — [PEP 8, the general Style Guide for Python Code](https://peps.python.org/pep-0008/) + +## Automatic linting + +As there is a lot of various code style rules we adhere to in our code base, describing all of them here would take way +too long and it would be impossible to remember anyway. For that reason, we use automated tools to help us catch any +code style violations automatically. + +Currently, we use [`ruff`](https://beta.ruff.rs/docs/) to enforce most of our code style requirements. That said, we do +have some other tools that check the correctness of the code, we will describe those later. + +### Ruff linter & formatter + +Ruff is an all-in-one linter & formatter solution, which aims to replace three previously very popular tools into a +single package: + +- [`flake8`](https://flake8.pycqa.org/en/latest/) linter +- [`isort`](https://pycqa.github.io/isort/) import sorter +- [`black`](https://black.readthedocs.io/en/stable/) auto-formatter + +??? question "Why pick ruff over the combination of these tools?" + + There were multiple reasons why we chose ruff instead of using the above tools individually, here's just some of + them: + + - Ruff is faster (written in rust! :crab:) + - A single tool is more convenient than 3 separate ones + - Ruff includes a lot of flake8 plugins with some great lint rules + - Ruff has a great community and is slowly managing to overtake these individual projects + - If you're already used to flake8, you'll feel right at home with ruff, it even has the same error codes (mostly)! + +You can check the ruff configuration we're using in `pyproject.toml` file, under the `[tool.ruff]` category (and it's +subcategories). You can find which linter rules are enabled and which we choose to exclude, some file-specific +overrides where the rules apply differently and a bunch of other configuration options. + +#### Linter + +To run ruff linter on the code, open the terminal in the project's root directory and run: + +```bash +ruff check . +``` + +!!! note "" + + Don't forget to [activate](./setup.md#activating-the-environment) the poetry virtual environment before running + ruff. + +Ruff is really smart and it can often automatically fix some of the style violations it found. To make ruff do that, +you can add the `--fix` flag to the command: + +```bash +ruff check --fix . +``` + +If you got a rule violation in your code and you don't understand what the rule's purpose is supposed to be / why we +enforce it, you can use Ruff to show you some details about that rule. The explanation that ruff will give you will +often even contain code examples. To achieve this, simply run: + +```bash +ruff rule [rule-id] +``` + +With the `[rule-id]` being the rule you're interested in, for example `UP038`. + +??? tip "Use glow to render the markdown syntax from ruff rule command" + + The `ruff rule` command will output the rule explanation in markdown, however, since you're running this comand + in a terminal, there won't be any helpful syntax highlighting for that by default. + + That's why I'd recommend using a markdown render such as [`glow`](https://github.com/charmbracelet/glow). With + it, you can pipe the output from ruff into it and have it produce a fancy colored output, that's much easier to + read: `ruff rule UP038 | glow`. + +Alternatively, you can also find the rules and their description in the [ruff +documentation](https://docs.astral.sh/ruff/rules/). + +#### Formatter + +On top of being an amazing linter, ruff is also an automatic code formatter. That means ruff can actually make your +code follow a proper and style automatically! It will just take your original unformatted (but valid) python code and +edit it to meet our configured code style for you. + +To make ruff format your code, simply run: + +```bash +ruff format . +``` + +## Other style guidelines + +While `ruff` can do a lot, it can't do everything. There are still some guidelines that you will need to read over and +apply manually. You will find these guides on the next pages of this documentation. From bd1110590d06f97047a516445c5523c958169feb Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 15:52:15 +0200 Subject: [PATCH 25/85] Add docstrings & other-tools (blank) pages --- docs/contributing/guides/docstrings.md | 5 +++++ docs/contributing/guides/other-tools.md | 5 +++++ mkdocs.yml | 4 +++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/contributing/guides/docstrings.md create mode 100644 docs/contributing/guides/other-tools.md diff --git a/docs/contributing/guides/docstrings.md b/docs/contributing/guides/docstrings.md new file mode 100644 index 00000000..09bb556a --- /dev/null +++ b/docs/contributing/guides/docstrings.md @@ -0,0 +1,5 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. + +# Docstring formatting directive diff --git a/docs/contributing/guides/other-tools.md b/docs/contributing/guides/other-tools.md new file mode 100644 index 00000000..05643db8 --- /dev/null +++ b/docs/contributing/guides/other-tools.md @@ -0,0 +1,5 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. + + diff --git a/mkdocs.yml b/mkdocs.yml index 6e7dc84a..d7d9c370 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,10 +22,12 @@ nav: - contributing/guides/index.md - Setting things up: contributing/guides/setup.md - Style Guide: contributing/guides/style-guide.md + - Docstring formatting: contributing/guides/docstrings.md - Type hinting: contributing/guides/type-hints.md - Pre-commit: contributing/guides/precommit.md - - Great commits: contributing/guides/great-commits.md + - Other tools: contributing/guides/other-tools.md - Changelog: contributing/guides/changelog.md + - Great commits: contributing/guides/great-commits.md - Unit Tests: contributing/guides/unit-tests.md - Deprecations: contributing/guides/deprecations.md From 4c52ba84dc686edfcef736ec171d4fbd0c835608 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 16:00:28 +0200 Subject: [PATCH 26/85] Add the PEP8 song! --- docs/contributing/guides/style-guide.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/contributing/guides/style-guide.md b/docs/contributing/guides/style-guide.md index 3e75fb09..af477474 100644 --- a/docs/contributing/guides/style-guide.md +++ b/docs/contributing/guides/style-guide.md @@ -25,6 +25,11 @@ you think a code style change of some kind would be justified, feel free to open — [PEP 8, the general Style Guide for Python Code](https://peps.python.org/pep-0008/) +??? tip "Check out the PEP8 song" + + The [Python Discord](https://www.pythondiscord.com/) community have made an amazing song about PEP8, check it out + [here](https://www.youtube.com/watch?v=hgI0p1zf31k)! + ## Automatic linting As there is a lot of various code style rules we adhere to in our code base, describing all of them here would take way From 4465d89ff1dd3a2d4eb8d3b47a0d626c9145cd27 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 16:00:42 +0200 Subject: [PATCH 27/85] Remove wip banner from style guide --- docs/contributing/guides/style-guide.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/contributing/guides/style-guide.md b/docs/contributing/guides/style-guide.md index af477474..9ecad330 100644 --- a/docs/contributing/guides/style-guide.md +++ b/docs/contributing/guides/style-guide.md @@ -1,7 +1,3 @@ -!!! bug "Work In Progress" - - This page is still being written. The content below (if any) may change. - # Style Guide ???+ abstract From 495410f6d98647ec49de48f1d8700baa7faa1ac8 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 17:47:36 +0200 Subject: [PATCH 28/85] Update licence 3rd party --- LICENSE-THIRD-PARTY.txt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/LICENSE-THIRD-PARTY.txt b/LICENSE-THIRD-PARTY.txt index 0fca57c3..cb47e9b7 100644 --- a/LICENSE-THIRD-PARTY.txt +++ b/LICENSE-THIRD-PARTY.txt @@ -15,13 +15,12 @@ Applies to: - .github/workflows/fragment-check.yml: Entire file - .github/workflows/prepare-release.yml: Workflow heavily inspired by original - .github/scripts/normalize_coverage.py: Entire file - - docs/_static/extra.css: Entire file - Copyright (c) 2015-present Rapptz All rights reserved. - - docs/pages/version_guarantees.rst: Entire file - - docs/_static/extra.css: Attribute table related config - - docs/_static/extra.js: Attribute table related functionality - - docs/extensions/attributetable.py: Entire file + - docs/installation/version-guarantees.rst: Entire file + - Copyright (c) 2016-2024 Martin Donath + All rights reserved + - docs/contributing/reporting-a-bug.md: Majority of the file --------------------------------------------------------------------------------------------------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From aa4147c2d8898ee0f102dd4d0fb7aaec06b6c1b8 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 18:07:24 +0200 Subject: [PATCH 29/85] Mention using an existing fork for new contributions --- docs/contributing/guides/setup.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/contributing/guides/setup.md b/docs/contributing/guides/setup.md index 4b5608ad..fbd4319d 100644 --- a/docs/contributing/guides/setup.md +++ b/docs/contributing/guides/setup.md @@ -20,10 +20,14 @@ requirement to work on this project. This guide assumes you have already [forked][github-forking] our repository, [clonned it][git-cloning] to your computer and created your own [git branch][git-branches] to work on. +If you wish to work from an already forked repository, make sure to check out the main branch and do a [`git pull`] to +get your fork up to date. Now create your new branch. + [git-and-github]: https://docs.github.com/en/get-started/start-your-journey/about-github-and-git [github-forking]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo [git-cloning]: https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository [git-branches]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches +[git-pull]: https://github.com/git-guides/git-pull ## Poetry From b9d64c353bbd8f933aaa79a21895c58b656628df Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 18:07:42 +0200 Subject: [PATCH 30/85] Explain CI & code reviews --- docs/contributing/making-a-pr.md | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/contributing/making-a-pr.md b/docs/contributing/making-a-pr.md index 8907d1da..16cd6313 100644 --- a/docs/contributing/making-a-pr.md +++ b/docs/contributing/making-a-pr.md @@ -63,3 +63,75 @@ Once your work is done and you think the PR is ready to be merged, mark it as ** ## Contributing guidelines In order to make a successful contribution, it is **required** that you get familiar with our [contributing guidelines](./guides/index.md). + +## Automated checks + +The project includes various CI workflows that will run automatically for your pull request after every push and check +your changed with various tools. These tools are here to ensure that our contributing guidelines are met and ensure +good code quality of your PR. + +That said, you shouldn't rely on these CI workflows to let you know if you made a mistake, instead, you should run +these tools on your own machine during the development. Many of these tools can fix the violations for you +automatically and it will generally be a better experience for you. Running these tools locally will also prevent a +bunch of "Fix the CI" commits, which just clutter the git history. + +Make sure to read our [contributing guidelines](./guides/index.md) thoroughly, as they describe how to use these tools +and even how to have them run automatically before each commit, so you won't forget. + +Passing the CI workflows is a requirement in order to get your pull request merged. If a maintainer sees a PR that's +marked as ready for review, but isn't passing the CI, we'll often refrain from even reviewing it, as we consider it +incomplete. If you have a technical reason why your PR can't pass the CI, let us know in the PR description or a +comment. + +## Code Review + +All pull requests will need to be reviewed by at least one team member before merging. The reviewer will provide +feedback and suggestions for improvement. + +Once a reviewer approves your pull request, it can be merged into the `main` branch. + +??? question "How do I request a review?" + + Request a review from a team member by [assigning them as a reviewer][assigning-pr-reviewer] to your pull request. + + However, you can also just wait until we get to your PR, you don't need to assign a reviewer unless you want + someone specific to review. Just make sure that your PR is marked as ready for review and not draft. + +[assigning-pr-reviewer]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review + +### Giving Feedback + +If you wish, you can also provide some feedback on other PRs. Doing so is a great way to fill the time while you're +waiting for your PR to be reviewed by us and you're also speeding up the process, as it reduces the amount of time +we'd have to spend reviewing those other PRs before getting to yours. + +When reviewing a pull request, aim to be constructive and specific. Highlight areas that need improvement and suggest +potential solutions. If you have any questions on concerns about something in the code, don't hesitate to ask the +author for clarification. + +Focus on the following aspects during a code review: + +- Correctness and functionality +- Code quality and readability +- Adherence to the project guidelines + +??? example "Good Code Review Feedback" + + Here are some examples of a good code review feedback: + + ``` + - Great work on the new function! The implementation looks good overall. + - The tests cover most of the functionality, but it's are missing a test case for edge case X. Could you add a test for that? + - The logic in the new function is somewhat complex. Consider breaking it into smaller functions for better clarity. + - The new feature is well-implemented, but it would be great to add more inline comments to explain the logic, as + it isn't trivial to understand. + - There's a small typo in the docstring. Could you correct it? + - The configuration settings are hard-coded. Can you move them to a configuration file to make it easier to manage? + ``` + +Always be respectful and considerate when giving feedback. Remember that the goal is to improve the code and help the +author grow as a developer. + +!!! success "Be Positive" + + Don't forget to acknowledge the positive aspects of the contribution as well! From d3fcbd35d0f7aa7f400f22f18f114739127b8e0b Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 4 Aug 2024 19:19:55 +0200 Subject: [PATCH 31/85] Add type hints page --- docs/contributing/guides/type-hints.md | 131 ++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/docs/contributing/guides/type-hints.md b/docs/contributing/guides/type-hints.md index fb61abca..db421628 100644 --- a/docs/contributing/guides/type-hints.md +++ b/docs/contributing/guides/type-hints.md @@ -1,3 +1,130 @@ -!!! bug "Work In Progress" +# Type Hints - This page is still being written. The content below (if any) may change. +???+ abstract + + This article explains what python type-hints are, how they can be enforced with the use of type checkers and the + type checker of our choice: **basedpyright** and it's editor integration. + +Most people only know python as a dynamically typed language, that doesn't offer any kind of type safety. In the very +days of python, this was true, however today, things are a bit different. Even though Python on it's own is still a +dynamically typed language, it does actually support specifying "type hints" which can even be enforced by external +tools called "type checkers". With those, we can achieve a (mostly) type safe experience while using Python. + +## Regular python + +In regular python, as most people know it, you might end up writing a function like this: + +```python +def add(x, y): + return x + y +``` + +In this code, you have no idea what the type of `x` and `y` arguments should be. So, even though you may have intended +for this function to only work with numbers (ints), it's actually entirely possible to use it with something else. For +example, running `add("hello", "world)` will return `"helloworld"` because the `+` operator works on strings too. + +The point is, there's nothing telling you what the type of these parameters should be, and that could lead to +misunderstandings. Even though in some cases, you can figure out what the type should these variables have purely based +on their name alongside the name of the function, in most cases, it's not that easy. It often requires looking through +the docs, or going over the actual source code of such function. + +Annoyingly, python won't even prevent you from passing in types that are definitely incorrect, like: `add(1, "hi")`. +Running this would cause a `TypeError`, but unless you have unit-tests that actually run that code, you won't find out +about this bug until it actually causes an issue and at that point, it might already be too late, since your code has +crashed a production app. + +Clearly then, this isn't ideal. + +## Type-Hints + +While python doesn't require it, there is in fact a way to add a "**hint**" that indicates what **type** should a given +variable have. So, when we take the function from above, adding type-hints to it would result in something like this: + +```python +def add(x: int, y: int) -> int: + return x + y +``` + +We've now made the types very explicit to the programmer, which means they'll no longer need to spend a bunch of time +looking through the implementation of that function, or going through the documentation just to know how to use this +function. Instead, the type hints will tell just you. + +This is incredibly useful, because most editors will be able to pick up these type hints, and show them to you while +calling the function, so you know what to pass right away, without even having to look at the function definition where +the type-hints are defined. + +Not only that, specifying a type-hint will greatly improve the development experience in your editor / IDE, because +you'll get much better auto-completion. The thing is, if you have a parameter like `x`, but your editor doesn't know +what type it should have, it can't really help you if you start typing `x.remove`, looking for the `removeprefix` +function. However, if you tell your editor that `x` is a string (`x: str`), it will now be able to go through all of +the methods that strings have, and show you those that start with `remove` (being `removeprefix` and `removesuffix`). + +This makes type-hints great at saving you time while developing, even though you have to do some additional work when +specifying them. + +## Runtime behavior + +Even though type-hints are a part of the Python language, the interpreter doesn't actually care about them. That means +that the interpreter doesn't do any optimizations or checking when you're running your code, even if you have a +function like `add` that we have added type-hints to, code like `add(1, "hi")` will not cause any immediate errors. + +Most editors are configured very loosely when it comes to type-hints. That means they will show you these hints when +you're working with the function, but they won't produce warnings when you pass in the wrong thing. That's why they're +called "type hints", they're only hints that can help you out, but they aren't actually enforced. + +## Enforcing type hints - Type Checkers + +Even though python on it's own indeed doesn't enforce the type-hints you specify, there are tools that can run "static" +checks against your code. A static check is a check that works with your code in it's textual form. It will read the +contents of your python files without actually running that file and analyze it purely based on that text content. + +Using these tools will allow you to analyze your code for typing mistakes before you ever even run your program. That +means having a function call like `add(1, "hi")` anywhere in your code would be detected and reported as an issue. + +There is a bunch of these tools available for python, but the most common ones are +[`pyright`](https://github.com/microsoft/pyright) and [`mypy`](https://mypy.readthedocs.io/en/stable/). + +## BasedPyright + +The type checker that we use in our code-base is [**basedpyright**](https://docs.basedpyright.com/). It's a fork of +pyright which adds some extra checks and features and focuses more on the open-source community, than the +official Microsoft owned Pyright. + +### Editor Integration + +=== "VSCode" + + On vscode, you can simply install the [BasedPyright extension][basedpyright-vscode-ext] from the marketplace. + + Note that this extension does collide with the commonly used **Pylance** extension, which is installed + automatically alongside the **Python** extension and provide intellisense for Python. The reason BasedPyright + collides with this extension is that Pylance actually uses pyright as a language server in the background, and as + we mentioned, basedpyright is an alternative, so using both would cause duplicate errors. This means that you will + need to disable Pylance, at least within our codebase. + +=== "Neovim" + + If you're using Neovim, I would recommend setting up LSP (Language Server Protocol) and installing basedpyright, as + it has language server support built into it. You can achieve this with the [`lspconfig`][neovim-lspconfig] plugin. + You can then use [`mason-lspconfig`][neovim-mason-lspconfig-plugin] to install `basedpyright`, or manually + configure `lspconfig` and use your system-wide `basedpyright` executable. + +[basedpyright-vscode-ext]: https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright +[neovim-lspconfig]: https://github.com/neovim/nvim-lspconfig +[neovim-mason-lspconfig-plugin]: https://github.com/williamboman/mason-lspconfig.nvim + +## Great resources + +While type hinting might seem very simple from the examples shown above, there is actually a fair bit to it, and if you +never worked within a type checked code-base, you should definitely check out some of these resources, which go over +the basics. + +- [Getting started with type hints in Python](https://dev.to/decorator_factory/type-hints-in-python-tutorial-3pel) - a + blog post / tutorial by decorator-factory. +- [Basics of static typing](https://docs.basedpyright.com/#/type-concepts) - part of the BasedPyright documentation +- [Mypy documentation](https://mypy.readthedocs.io/en/stable/) - very extensive documentation on various typing + concepts. (Some things are mypy focused, but most things will cary over to basedpyright too) +- [Python documentation for the `typing` module](https://docs.python.org/3/library/typing.html) - Python's standard + library contains a `typing` module, which holds a bunch of useful structures that we often use while working with + type-hints. +- [PEP 484](https://www.python.org/dev/peps/pep-0484/) - formal specification of type hints for the Python langauge From 8419d9078d9d197f1eac3d8ce88339428ae414dc Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 14:20:53 +0200 Subject: [PATCH 32/85] Update installation commands --- docs/installation/index.md | 96 +++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/docs/installation/index.md b/docs/installation/index.md index 58651b28..24d48151 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -4,24 +4,66 @@ Mcproto is available on [PyPI](https://pypi.org/project/mcproto) and can be installed like any other python library with: -=== "pip" +=== ":simple-python: pip" ```bash pip install mcproto ``` -=== "poetry" +
+ + [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python. + +
+ +=== ":simple-poetry: poetry" ```bash poetry add mcproto ``` -=== "rye" +
+ + [Poetry](https://python-poetry.org/) is an all-in-one solution for Python project management. + +
+ +=== ":simple-rye: rye" ```bash rye add mcproto ``` +
+ + [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust. + +
+ +=== ":simple-ruff: uv" + + ```bash + uv pip install mcproto + ``` + +
+ + [uv](https://github.com/astral-sh/uv) is an ultra fast dependency resolver and package installer, written in Rust. + +
+ +=== ":simple-pdm: pdm" + + ```bash + pdm add mcproto + ``` + +
+ + [PDM](https://pdm-project.org/en/latest/) is an all-in-one solution for Python project management. + +
+ ## Latest (git) version Alternatively, you may want to install the latest available version, which is what you currently see in the `main` git @@ -36,20 +78,62 @@ you'll want. To install the latest mcproto version directly from the `main` git branch, use: -=== "pip" +=== ":simple-python: pip" ```bash pip install 'mcproto@git+https://github.com/py-mine/mcproto@main' ``` -=== "poetry" +
+ + [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python. + +
+ +=== ":simple-poetry: poetry" ```bash poetry add 'git+https://github.com/py-mine/mcproto#main' ``` -=== "rye" +
+ + [Poetry](https://python-poetry.org/) is an all-in-one solution for Python project management. + +
+ +=== ":simple-rye: rye" ```bash rye add mcproto --git='https://github.com/py-mine/mcproto' --branch main ``` + +
+ + [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust. + +
+ +=== ":simple-ruff: uv" + + ```bash + uv pip install 'mcproto@git+https://github.com/py-mine/mcproto@main' + ``` + +
+ + [uv](https://github.com/astral-sh/uv) is an ultra fast dependency resolver and package installer, written in Rust. + +
+ +=== ":simple-pdm: pdm" + + ```bash + pdm add "git+https://github.com/py-mine/mcproto@main" + ``` + +
+ + [PDM](https://pdm-project.org/en/latest/) is an all-in-one solution for Python project management. + +
From 39635086c175ae3379e4df2b08acee65a225eccc Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 15:45:15 +0200 Subject: [PATCH 33/85] Use markdown-exec to dynamically run python code in docs --- docs/installation/changelog.md | 13 ++----- docs/scripts/gen_changelog.py | 62 ++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 ++ poetry.lock | 34 ++++++++++++++++++- pyproject.toml | 10 +++--- 5 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 docs/scripts/gen_changelog.py diff --git a/docs/installation/changelog.md b/docs/installation/changelog.md index f70d8fe0..83255362 100644 --- a/docs/installation/changelog.md +++ b/docs/installation/changelog.md @@ -4,15 +4,8 @@ Major and minor releases also include the changes specified in prior development releases. -!!! bug "Missing unreleased changes" - - This changelog doesn't contain any unreleased (pending) changes, even if they are present in this version of the - project documentation already. If you are interested in knowing what these changes are, you can take a look at the - [`changes/`](https://github.com/py-mine/mcproto/tree/main/changes) directory of the project. - - In the future, we want to display these changes properly in this documentation, however, doing so requires running - a command dynamically, and showing it's output here, which we currently don't support. - - +```python exec="yes" +--8<-- "docs/scripts/gen_changelog.py" +``` --8<-- "CHANGELOG.md" diff --git a/docs/scripts/gen_changelog.py b/docs/scripts/gen_changelog.py new file mode 100644 index 00000000..c03cb813 --- /dev/null +++ b/docs/scripts/gen_changelog.py @@ -0,0 +1,62 @@ +"""Script to generate a draft towncrier changelog for the next release. + +This script is intended to be ran by mkdocs to generate a markdown output that will be included +in the changelog page of the documentation. + +(The script is executed from the project root directory, so the paths are relative to that) +""" + +import subprocess + +INDENT_PREFIX = " " # we use 4 spaces for single indent + + +def get_project_version() -> str: + """Get project version using git describe. + + This will obtain a version named according to the latest version tag, + followed by the number of commits since that tag, and the latest commit hash. + (e.g. v0.5.0-166-g26b88) + """ + proc = subprocess.run( # noqa: S603 + ["git", "describe", "--tags", "--abbrev=5"], # noqa: S607 + capture_output=True, + check=True, + ) + proc.check_returncode() + out = proc.stdout.decode().strip() + if out == "": + raise ValueError("Could not get project version") + return out + + +def get_changelog(version: str) -> str: + """Generate draft changelog for the given project version.""" + proc = subprocess.run( # noqa: S603 + ["towncrier", "build", "--draft", "--version", version], # noqa: S607 + capture_output=True, + check=True, + ) + proc.check_returncode() + + changes = proc.stdout.decode().strip() + if changes == "": + raise ValueError("Could not generate changelog") + + header, changes = changes.split("\n", maxsplit=1) + changes = changes.lstrip() + + if changes.startswith("No significant changes"): + return "" + + # Wrap the changes output into an admonition block + admonition_header = '???+ example "Unreleased Changes"' + + # Prefix each line with a tab to make it part of the admonition block + header = f"{INDENT_PREFIX}{header}" + changes = "\n".join(f"{INDENT_PREFIX}{line}" for line in changes.split("\n")) + + return admonition_header + "\n" + header + "\n\n" + changes + + +print(get_changelog(get_project_version())) diff --git a/mkdocs.yml b/mkdocs.yml index d7d9c370..c731b8ca 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,6 +94,8 @@ markdown_extensions: plugins: - search + - markdown-exec: + ansi: required - mike: canonical_version: "latest" version_selector: true diff --git a/poetry.lock b/poetry.lock index 83950537..c62c2a7c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -680,6 +680,24 @@ importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-exec" +version = "1.10.0" +description = "Utilities to execute code blocks in Markdown files." +optional = false +python-versions = ">=3.9" +files = [ + {file = "markdown_exec-1.10.0-py3-none-any.whl", hash = "sha256:dea4e8b78a3fe7d8e664088ebaccbd4de51b65c45b9e0db9509a9bb4fce33192"}, + {file = "markdown_exec-1.10.0.tar.gz", hash = "sha256:d1fa017995ef337ec59e7ce49fbf3e051145a62c3124ae687c17e987f1392cd0"}, +] + +[package.dependencies] +pygments-ansi-color = {version = "*", optional = true, markers = "extra == \"ansi\""} +pymdown-extensions = ">=9" + +[package.extras] +ansi = ["pygments-ansi-color"] + [[package]] name = "markupsafe" version = "2.1.5" @@ -1072,6 +1090,20 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pygments-ansi-color" +version = "0.3.0" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-ansi-color-0.3.0.tar.gz", hash = "sha256:7018954cf5b11d1e734383a1bafab5af613213f246109417fee3f76da26d5431"}, + {file = "pygments_ansi_color-0.3.0-py3-none-any.whl", hash = "sha256:7eb063feaecadad9d4d1fd3474cbfeadf3486b64f760a8f2a00fc25392180aba"}, +] + +[package.dependencies] +pygments = "!=2.7.3" + [[package]] name = "pymdown-extensions" version = "10.14.3" @@ -1683,4 +1715,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "347422a4ca864526c3a004a89f995af02799640bea7819ad6c712ed98ee2888f" +content-hash = "7e6ec02280b38874a8cbf3bb26ee55fae6f605cff28a14d35e1097b0d858394f" diff --git a/pyproject.toml b/pyproject.toml index 9a26182a..21ab97b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ poetry-dynamic-versioning = ">=1.4.0,<1.8" mkdocs = "^1.6.0" mkdocs-material = "^9.5.30" mike = "^2.1.2" +markdown-exec = { extras = ["ansi"], version = "^1.9.3" } [tool.poetry.group.docs-ci] optional = true @@ -188,12 +189,9 @@ ignore = [ "ANN", # flake8-annotations "S101", # Use of assert ] -"docs/conf.py" = [ - "INP", # allow implicit namespace (pep 420) -] -"docs/extensions/*" = [ - "D", # pydocstyle - "INP", # allow implicit namespace (pep 420) +"docs/scripts/*" = [ + "INP", # allow implicit namespace (pep 420) + "T201", # allow prints ] ".github/scripts/*" = [ "D", # pydocstyle From 953fcfb21c8f7a4295ab61d201da9742fa6ec4da Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 16:10:15 +0200 Subject: [PATCH 34/85] Update changelog & change fragments to render properly --- CHANGELOG.md | 90 +++++++++++++++++++-------------------- changes/209.feature.md | 2 +- changes/257.feature.md | 25 +++++------ changes/274.internal.md | 9 ++-- changes/285.internal.1.md | 35 +-------------- changes/285.internal.2.md | 17 +------- changes/300.internal.md | 14 +++--- 7 files changed, 74 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c93807c..b4bc6ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,22 +4,24 @@ - [#130](https://github.com/py-mine/mcproto/issues/130): Renamed "shared_key" field to "shared_secret" in `LoginEncryptionPacket`, following the official terminology. - - This is a breaking change, `LoginEncryptionPacket`'s `__init__` method now uses "shared_secret" keyword only argument, not "shared_key". + - This is a breaking change, `LoginEncryptionPacket`'s `__init__` method now uses "shared_secret" keyword only argument, not "shared_key". + - [#130](https://github.com/py-mine/mcproto/issues/130): The `LoginStart` packet now contains a (required) UUID field (which can be explicitly set to `None`). - - For some reason, this field was not added when the login packets were introduced initially, and while the UUID field can indeed be omitted in some cases (it is an optional filed), in vast majority of cases, it will be present, and we should absolutely support it. - - As this is a new required field, the `__init__` function of `LoginStart` now also expects this `uuid` keyword argument to be present, making this a breaking change. + - For some reason, this field was not added when the login packets were introduced initially, and while the UUID field can indeed be omitted in some cases (it is an optional filed), in vast majority of cases, it will be present, and we should absolutely support it. + - As this is a new required field, the `__init__` function of `LoginStart` now also expects this `uuid` keyword argument to be present, making this a breaking change. - [#159](https://github.com/py-mine/mcproto/issues/159): Fix packet compression handling in the interaction methods. - This fixes a bug that didn't allow for specifying an exact compression threshold that the server specified in `LoginSetCompression` packet, and instead only allowing to toggle between compression on/off, which doesn't really work as server doesn't expect compression for packets below that threshold. + This fixes a bug that didn't allow for specifying an exact compression threshold that the server specified in `LoginSetCompression` packet, and instead only allowing to toggle between compression on/off, which doesn't really work as server doesn't expect compression for packets below that threshold. + + - `sync_write_packet`, `async_write_pakcet`, `sync_read_packet` and `async_read_packet` functions now take `compression_threshold` instead of `compressed` bool flag - - `sync_write_packet`, `async_write_pakcet`, `sync_read_packet` and `async_read_packet` functions now take `compression_threshold` instead of `compressed` bool flag - [#161](https://github.com/py-mine/mcproto/issues/161): `LoginEncryptionRequest` now uses `cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` to hold the public key, instead of just pure `bytes`. Encoding and decoding of this key happens automatically during serialize/deserialize. This is a breaking change for anyone relying on the `public_key` field from this packet being `bytes`, and for anyone initializing this packet directly with `__init__`, which now expects `RSAPublicKey` instance instead. ### Features - [#129](https://github.com/py-mine/mcproto/issues/129): Added a system for handling Minecraft authentication - - Yggdrasil system for unmigrated i.e. non-Microsoft accounts (supportng Minecraft accounts, and the really old Mojang accounts) - - Microsoft OAuth2 system (Xbox live) for migrated i.e. Microsoft accounts + - Yggdrasil system for unmigrated i.e. non-Microsoft accounts (supportng Minecraft accounts, and the really old Mojang accounts) + - Microsoft OAuth2 system (Xbox live) for migrated i.e. Microsoft accounts - [#160](https://github.com/py-mine/mcproto/issues/160): Re-export the packet classes (or any other objects) from the gamestate modules (`mcproto.packets.handshaking`/`mcproto.packets.login`/...) directly. Allowing simpler imports (`from mcproto.packets.login import LoginStart` instead of `from mcproto.packets.login.login import LoginStart`) - [#161](https://github.com/py-mine/mcproto/issues/161): Add support for encryption. Connection classes now have `enable_encryption` method, and some encryption related functions were added into a new `mcproto.encryption` module. - [#168](https://github.com/py-mine/mcproto/issues/168): Add multiplayer related functionalities for requesting and checking joins for original (bought) minecraft accounts. This allows us to join online servers. @@ -28,7 +30,7 @@ ### Bugfixes - [#130](https://github.com/py-mine/mcproto/issues/130): `LoginEncryptionResponse` now includes the `server_id` field. This field was previously hard-coded to 20 spaces (blank value), which is what all minecraft clients on minecraft 1.7.x or higher do, however with older versions, this field is set to 20 random characters, which we should respect. - - This is not a breaking change, as `server_id` will default to `None` in `LoginEncryptionResponse`'s `__init__`, meaning any existing code utilizing this packet will still work. It is purely an additional option. + - This is not a breaking change, as `server_id` will default to `None` in `LoginEncryptionResponse`'s `__init__`, meaning any existing code utilizing this packet will still work. It is purely an additional option. - [#167](https://github.com/py-mine/mcproto/issues/167): Fix packet reading/writing when compression is enabled (use zlib as expected, instead of gzip which we were using before) - [#170](https://github.com/py-mine/mcproto/issues/170): Preserve the call parameters and overloads in the typing signature of `mcproto.packets.packet_map.generate_packet_map` function. (This wasn't the case before, since `functools.lru_cache` doesn't preserve this data). Note that this loses on the typing information about the cache itself, as now it will appear to be a regular uncached function to the type-checker. We deemed this approach better to the alternative of no typing info for call arguments or overloads, but preserving cache info. @@ -39,8 +41,9 @@ - [#141](https://github.com/py-mine/mcproto/issues/141): Move installation instructions from README to Installation docs page - [#144](https://github.com/py-mine/mcproto/issues/144): Add attributetable internal sphinx extension for showing all attributes and methods for specified classes. - - This adds `attributetable` sphinx directive, which can be used before autodoc directive. This will create the attribute table, which will get dynamically moved right below the class definition from autodoc (using javascript). - - This extension was implemented by [discord.py](https://github.com/Rapptz/discord.py/blob/2fdbe59376d736483cd1226e674e609433877af4/docs/extensions/attributetable.py), this is just re-using that code, with some modifications to fit our code style and to fit the documentation design (furo theme). + - This adds `attributetable` sphinx directive, which can be used before autodoc directive. This will create the attribute table, which will get dynamically moved right below the class definition from autodoc (using javascript). + - This extension was implemented by [discord.py](https://github.com/Rapptz/discord.py/blob/2fdbe59376d736483cd1226e674e609433877af4/docs/extensions/attributetable.py), this is just re-using that code, with some modifications to fit our code style and to fit the documentation design (furo theme). + - Updated contributing guidelines (restructure and rewrite some categories, to make it more readable) ### Internal Changes @@ -49,45 +52,43 @@ - [#153](https://github.com/py-mine/mcproto/issues/153): Replace flake8 linter with ruff (mostly equivalent, but much faster and configurable from pyproject.toml) - [#154](https://github.com/py-mine/mcproto/issues/154): Enforce various new ruff linter rules: - - **PGH:** pygrep-hooks (replaces pre-commit version) - - **PL:** pylint (bunch of typing related linter rules) - - **UP:** pyupgrade (forces use of the newest possible standards, depending on target version) - - **RET:** flake8-return (various linter rules related to function returns) - - **Q:** flake8-quotes (always use double quotes) - - **ASYNC:** flake8-async (report blocking operations in async functions) - - **INT:** flake-gettext (gettext related linting rules) - - **PTH:** flake8-use-pathlib (always prefer pathlib alternatives to the os ones) - - **RUF:** ruff custom rules (various additional rules created by the ruff linter team) + - **PGH:** pygrep-hooks (replaces pre-commit version) + - **PL:** pylint (bunch of typing related linter rules) + - **UP:** pyupgrade (forces use of the newest possible standards, depending on target version) + - **RET:** flake8-return (various linter rules related to function returns) + - **Q:** flake8-quotes (always use double quotes) + - **ASYNC:** flake8-async (report blocking operations in async functions) + - **INT:** flake-gettext (gettext related linting rules) + - **PTH:** flake8-use-pathlib (always prefer pathlib alternatives to the os ones) + - **RUF:** ruff custom rules (various additional rules created by the ruff linter team) --- - ## Version 0.4.0 (2023-06-11) ### Breaking Changes - [#41](https://github.com/py-mine/mcproto/issues/41): Rename `mcproto.packets.abc` to `mcproto.packets.packet` - [#116](https://github.com/py-mine/mcproto/issues/116): Restructure the project, moving to a single protocol version model - - This change does NOT have a deprecation period, and will very likely break most existing code-bases. However this change is necessary, as multi-version support was unsustainable (see issue #45 for more details) - - Any packets and types will no longer be present in versioned folders (mcproto.packets.v757.xxx), but rather be directly in the parent directory (mcproto.packets.xxx). - - This change doesn't affect manual communication with the server, connection, and basic IO writers/readers remain the same. + - This change does NOT have a deprecation period, and will very likely break most existing code-bases. However this change is necessary, as multi-version support was unsustainable (see issue #45 for more details) + - Any packets and types will no longer be present in versioned folders (mcproto.packets.v757.xxx), but rather be directly in the parent directory (mcproto.packets.xxx). + - This change doesn't affect manual communication with the server, connection, and basic IO writers/readers remain the same. --- - ## Version 0.3.0 (2023-06-08) ### Features - [#54](https://github.com/py-mine/mcproto/issues/54): Add support for LOGIN state packets - - `LoginStart` - - `LoginEncryptionRequest` - - `LoginEncryptionResponse` - - `LoginSuccess` - - `LoginDisconnect` - - `LoginPluginRequest` - - `LoginPluginResponse` - - `LoginSetCompression` + - `LoginStart` + - `LoginEncryptionRequest` + - `LoginEncryptionResponse` + - `LoginSuccess` + - `LoginDisconnect` + - `LoginPluginRequest` + - `LoginPluginResponse` + - `LoginSetCompression` ### Bugfixes @@ -103,9 +104,9 @@ - [#34](https://github.com/py-mine/mcproto/issues/34): Add version guarantees page - [#40](https://github.com/py-mine/mcproto/issues/40): Move code of conduct to the docs. - Improve readability of the changelog readme (changes/README.md) - - Mention taskipy `changelog-preview` shorthand command - - Add category headers splitting things up, for better readability - - Explain how to express multiple changes related to a single goal in a changelog fragment. + - Mention taskipy `changelog-preview` shorthand command + - Add category headers splitting things up, for better readability + - Explain how to express multiple changes related to a single goal in a changelog fragment. - Include `CHANGELOG.md` file in project's distribution files. ### Internal Changes @@ -121,14 +122,13 @@ --- - ## Version 0.2.0 (2022-12-30) ### Features - [#14](https://github.com/py-mine/mcproto/issues/14): Add `__slots__` to most classes in the project - - All connection classes are now slotted - - Classes in `mcproto.utils.abc` are now slotted + - All connection classes are now slotted + - Classes in `mcproto.utils.abc` are now slotted - Separate packet interaction functions into `mcproto.packets.interactions`, (though they're reexported in `mcproto.packets`, so no breaking changes) @@ -147,11 +147,11 @@ ### Internal Changes - [#6](https://github.com/py-mine/mcproto/issues/6): Rework deprecation system - - Drop support for date-based deprecations, versions work better - - Provide `deprecation_warn` function, which emits warnings directly, no need for a decorator - - Add a `SemanticVersion` class, supporting version comparisons - - If the project's version is already higher than the specified deprecation removal version, raise a DeprecationWarning - as a full exception (rather than just a warning). + - Drop support for date-based deprecations, versions work better + - Provide `deprecation_warn` function, which emits warnings directly, no need for a decorator + - Add a `SemanticVersion` class, supporting version comparisons + - If the project's version is already higher than the specified deprecation removal version, raise a DeprecationWarning + as a full exception (rather than just a warning). - [#7](https://github.com/py-mine/mcproto/issues/7): Add towncrier for managing changelog - [#14](https://github.com/py-mine/mcproto/issues/14): Add slotscheck, ensuring `__slots__` are defined properly everywhere. - [#14](https://github.com/py-mine/mcproto/issues/14): Make `typing-extensions` a runtime dependency and use it directly, don't rely on `if typing.TYPE_CHECKING` blocks. @@ -164,5 +164,5 @@ --- -*The changelog was added during development of 0.2.0, so nothing prior is documented here. Try checking the GitHub -releases, or git commit history directly.* +_The changelog was added during development of 0.2.0, so nothing prior is documented here. Try checking the GitHub +releases, or git commit history directly._ diff --git a/changes/209.feature.md b/changes/209.feature.md index 4ea701de..f126c3ad 100644 --- a/changes/209.feature.md +++ b/changes/209.feature.md @@ -1 +1 @@ -- Added `InvalidPacketContentError` exception, raised when deserializing of a specific packet fails. This error inherits from `IOError`, making it backwards compatible with the original implementation. +Added `InvalidPacketContentError` exception, raised when deserializing of a specific packet fails. This error inherits from `IOError`, making it backwards compatible with the original implementation. diff --git a/changes/257.feature.md b/changes/257.feature.md index 453b22bf..01ac334e 100644 --- a/changes/257.feature.md +++ b/changes/257.feature.md @@ -1,12 +1,13 @@ -- Added the `NBTag` to deal with NBT data: - - The `NBTag` class is the base class for all NBT tags and provides the basic functionality to serialize and deserialize NBT data from and to a `Buffer` object. - - The classes `EndNBT`, `ByteNBT`, `ShortNBT`, `IntNBT`, `LongNBT`, `FloatNBT`, `DoubleNBT`, `ByteArrayNBT`, `StringNBT`, `ListNBT`, `CompoundNBT`, `IntArrayNBT`and `LongArrayNBT` were added and correspond to the NBT types described in the [NBT specification](https://wiki.vg/NBT#Specification). - - NBT tags can be created using the `NBTag.from_object()` method and a schema that describes the NBT tag structure. - Compound tags are represented as dictionaries, list tags as lists, and primitive tags as their respective Python types. - The implementation allows to add custom classes to the schema to handle custom NBT tags if they inherit the `:class: NBTagConvertible` class. - - The `NBTag.to_object()` method can be used to convert an NBT tag back to a Python object. Use include_schema=True to include the schema in the output, and `include_name=True` to include the name of the tag in the output. In that case the output will be a dictionary with a single key that is the name of the tag and the value is the object representation of the tag. - - The `NBTag.serialize()` can be used to serialize an NBT tag to a new `Buffer` object. - - The `NBTag.deserialize(buffer)` can be used to deserialize an NBT tag from a `Buffer` object. - - If the buffer already exists, the `NBTag.write_to(buffer, with_type=True, with_name=True)` method can be used to write the NBT tag to the buffer (and in that case with the type and name in the right format). - - The `NBTag.read_from(buffer, with_type=True, with_name=True)` method can be used to read an NBT tag from the buffer (and in that case with the type and name in the right format). - - The `NBTag.value` property can be used to get the value of the NBT tag as a Python object. +Added the `NBTag` to deal with NBT data + + - The `NBTag` class is the base class for all NBT tags and provides the basic functionality to serialize and deserialize NBT data from and to a `Buffer` object. + - The classes `EndNBT`, `ByteNBT`, `ShortNBT`, `IntNBT`, `LongNBT`, `FloatNBT`, `DoubleNBT`, `ByteArrayNBT`, `StringNBT`, `ListNBT`, `CompoundNBT`, `IntArrayNBT`and `LongArrayNBT` were added and correspond to the NBT types described in the [NBT specification](https://wiki.vg/NBT#Specification). + - NBT tags can be created using the `NBTag.from_object()` method and a schema that describes the NBT tag structure. + Compound tags are represented as dictionaries, list tags as lists, and primitive tags as their respective Python types. + The implementation allows to add custom classes to the schema to handle custom NBT tags if they inherit the `:class: NBTagConvertible` class. + - The `NBTag.to_object()` method can be used to convert an NBT tag back to a Python object. Use include_schema=True to include the schema in the output, and `include_name=True` to include the name of the tag in the output. In that case the output will be a dictionary with a single key that is the name of the tag and the value is the object representation of the tag. + - The `NBTag.serialize()` can be used to serialize an NBT tag to a new `Buffer` object. + - The `NBTag.deserialize(buffer)` can be used to deserialize an NBT tag from a `Buffer` object. + - If the buffer already exists, the `NBTag.write_to(buffer, with_type=True, with_name=True)` method can be used to write the NBT tag to the buffer (and in that case with the type and name in the right format). + - The `NBTag.read_from(buffer, with_type=True, with_name=True)` method can be used to read an NBT tag from the buffer (and in that case with the type and name in the right format). + - The `NBTag.value` property can be used to get the value of the NBT tag as a Python object. diff --git a/changes/274.internal.md b/changes/274.internal.md index c4892e10..9a45750f 100644 --- a/changes/274.internal.md +++ b/changes/274.internal.md @@ -1,4 +1,5 @@ -- Update ruff version (the version we used was very outdated) -- Drop isort in favor of ruff's built-in isort module in the linter -- Drop black in favor of ruff's new built-in formatter -- Update ruff settings, including adding/enabling some new rule-sets +Update ruff + - Update ruff version (the version we used was very outdated) + - Drop isort in favor of ruff's built-in isort module in the linter + - Drop black in favor of ruff's new built-in formatter + - Update ruff settings, including adding/enabling some new rule-sets diff --git a/changes/285.internal.1.md b/changes/285.internal.1.md index 5536712b..8e1372f7 100644 --- a/changes/285.internal.1.md +++ b/changes/285.internal.1.md @@ -1,34 +1 @@ -- **Function**: `gen_serializable_test` - - Generates tests for serializable classes, covering serialization, deserialization, validation, and error handling. - - **Parameters**: - - `context` (dict): Context to add the test functions to (usually `globals()`). - - `cls` (type): The serializable class to test. - - `fields` (list): Tuples of field names and types of the serializable class. - - `serialize_deserialize` (list, optional): Tuples for testing successful serialization/deserialization. - - `validation_fail` (list, optional): Tuples for testing validation failures with expected exceptions. - - `deserialization_fail` (list, optional): Tuples for testing deserialization failures with expected exceptions. - - **Note**: Implement `__eq__` in the class for accurate comparison. - - - The `gen_serializable_test` function generates a test class with the following tests: - -.. literalinclude:: /../tests/mcproto/utils/test_serializable.py - :language: python - :start-after: # region Test ToyClass - :end-before: # endregion Test ToyClass - - - The generated test class will have the following tests: - -```python -class TestGenToyClass: - def test_serialization(self): - # 3 subtests for the cases 1, 2, 3 (serialize_deserialize) - - def test_deserialization(self): - # 3 subtests for the cases 1, 2, 3 (serialize_deserialize) - - def test_validation(self): - # 3 subtests for the cases 4, 5, 6 (validation_fail) - - def test_exceptions(self): - # 3 subtests for the cases 7, 8, 9 (deserialization_fail) -``` +Add `gen_serializable_test` function to generate tests for serializable classes, covering serialization, deserialization, validation, and error handling. diff --git a/changes/285.internal.2.md b/changes/285.internal.2.md index 7969f33c..c0eb7b9e 100644 --- a/changes/285.internal.2.md +++ b/changes/285.internal.2.md @@ -1,16 +1 @@ -- **Class**: `Serializable` - - Base class for types that should be (de)serializable into/from `mcproto.Buffer` data. - - **Methods**: - - `__attrs_post_init__()`: Runs validation after object initialization, override to define custom behavior. - - `serialize() -> Buffer`: Returns the object as a `Buffer`. - - `serialize_to(buf: Buffer)`: Abstract method to write the object to a `Buffer`. - - `validate()`: Validates the object's attributes; can be overridden for custom validation. - - `deserialize(cls, buf: Buffer) -> Self`: Abstract method to construct the object from a `Buffer`. - - **Note**: Use the `dataclass` decorator when adding parameters to subclasses. - - - Exemple: - -.. literalinclude:: /../tests/mcproto/utils/test_serializable.py - :language: python - :start-after: # region ToyClass - :end-before: # endregion ToyClass +Rework the `Serializable` class diff --git a/changes/300.internal.md b/changes/300.internal.md index cb1372e3..4f005ece 100644 --- a/changes/300.internal.md +++ b/changes/300.internal.md @@ -1,6 +1,8 @@ -- Fix CI not running unit tests on python 3.8 (only 3.11) -- Update to use python 3.12 (in validation and as one of the matrix versions in unit-tests workflow) -- Trigger and run lint and unit-tests workflows form a single main CI workflow. -- Only send status embed after the main CI workflow finishes (not for both unit-tests and validation) -- Use `--output-format=github` for `ruff check` in the validation workflow -- Fix the status-embed workflow +Update CI + + - Fix CI not running unit tests on python 3.8 (only 3.11) + - Update to use python 3.12 (in validation and as one of the matrix versions in unit-tests workflow) + - Trigger and run lint and unit-tests workflows form a single main CI workflow. + - Only send status embed after the main CI workflow finishes (not for both unit-tests and validation) + - Use `--output-format=github` for `ruff check` in the validation workflow + - Fix the status-embed workflow From b184130cd12a24406c850806cb094a74c68aa6e7 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 16:13:34 +0200 Subject: [PATCH 35/85] Watch some external files for reloading docs --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index c731b8ca..ea0ae5a3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,6 +4,8 @@ site_url: https://py-mine.github.io/mcproto repo_url: https://github.com/py-mine/mcproto repo_name: py-mine/mcproto +watch: [LICENSE.txt, LICENSE-THIRD-PARTY.txt, ATTRIBUTION.md, CHANGELOG.md, changes] + nav: - Home: index.md - Installation: From a66ad3f7a070aff3c7b0930f547f879d4117ad29 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 16:27:12 +0200 Subject: [PATCH 36/85] Fetch the entire git history for mkdocs workflow --- .github/workflows/mkdocs.yml | 8 +++++--- .github/workflows/publish.yml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index cd81c0f4..842d646b 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -30,9 +30,11 @@ jobs: uses: actions/checkout@v4 with: token: "${{ steps.app-token.outputs.token }}" - - - name: Fetch gh-pages branch - run: git fetch origin gh-pages + # Fetch the entire git history (all branches + tags) + # We do this because the docs use git describe, which requires having all + # the commits up to the latest version tag. + # We also need the gh-pages branch to push the docs to. + fetch-depth: 0 # Make the github application be the committer # (see: https://stackoverflow.com/a/74071223 on how to obtain the committer email) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4247678b..68918386 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -152,9 +152,11 @@ jobs: uses: actions/checkout@v4 with: token: "${{ steps.app-token.outputs.token }}" - - - name: Fetch gh-pages branch - run: git fetch origin gh-pages + # Fetch the entire git history (all branches + tags) + # We do this because the docs use git describe, which requires having all + # the commits up to the latest version tag. + # We also need the gh-pages branch to push the docs to. + fetch-depth: 0 # Make the github application be the committer # (see: https://stackoverflow.com/a/74071223 on how to obtain the committer email) From ff21bbc780e8896bd5be6153989a0a9e709b5e47 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 16:31:30 +0200 Subject: [PATCH 37/85] Add towncrier to docs dependencies --- poetry.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index c62c2a7c..9771bc7e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1715,4 +1715,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "7e6ec02280b38874a8cbf3bb26ee55fae6f605cff28a14d35e1097b0d858394f" +content-hash = "6921a1cb55aace4b6cbaeaada7508db952769b10e5cfe6f6e045ef63c5ae590e" diff --git a/pyproject.toml b/pyproject.toml index 21ab97b5..5f422f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ mkdocs = "^1.6.0" mkdocs-material = "^9.5.30" mike = "^2.1.2" markdown-exec = { extras = ["ansi"], version = "^1.9.3" } +towncrier = ">=23,<24.7" # temporary pin, as 24.7 is incompatible with sphinxcontrib-towncrier [tool.poetry.group.docs-ci] optional = true From 85031a253e386c1c215091580cc314d8605182d3 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 17:28:22 +0200 Subject: [PATCH 38/85] Fix reference --- docs/contributing/guides/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/guides/index.md b/docs/contributing/guides/index.md index 5d717d18..365572d1 100644 --- a/docs/contributing/guides/index.md +++ b/docs/contributing/guides/index.md @@ -33,7 +33,7 @@ rule. 2. **Make great commits.** Great commits should be atomic (do one thing only and do it well), with a commit message that explaining what was done, and why. More on this [here](./great-commits.md). 3. **Make an issue before the PR.** Before you start working on your PR, open an issue and let us know what you're - planning. We described this further in our [making a PR guide](../making-a-pr.md##get-assigned-to-the-issue). + planning. We described this further in our [making a PR guide](../making-a-pr.md#get-assigned-to-the-issue). 4. **Use assets licensed for public use.** Whenever you're adding a static asset (e.g. images/video files/audio or even code) that isn't owned/written by you, make sure it has a compatible license with our projects. 5. **Follow our [Code of Conduct](../../community/code-of-conduct.md)** From de07bef3f2ff348ceebfd467c63e2fc0800d78b0 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 18:45:59 +0200 Subject: [PATCH 39/85] Add changelog explanation --- changes/README.md | 72 +------- docs/contributing/guides/changelog.md | 257 +++++++++++++++++++++++++- 2 files changed, 256 insertions(+), 73 deletions(-) diff --git a/changes/README.md b/changes/README.md index 4ce9df02..89264b57 100644 --- a/changes/README.md +++ b/changes/README.md @@ -3,74 +3,4 @@ This folder holds fragments of the changelog to be used in the next release, when the final changelog will be generated. -For every pull request made to this project, the contributor is responsible for creating a file (fragment), with -a short description of what that PR changes. - -These fragment files use the following format: `{pull_request_number}.{type}.md`, - -Possible types are: -- **`feature`**: New feature that affects the public API. -- **`bugfix`**: A bugfix, which was affecting the public API. -- **`docs`**: Change to the documentation, or updates to public facing docstrings -- **`breaking`**: Signifying a breaking change of some part of the project's public API, which could cause issues for - end-users updating to this version. (Includes deprecation removals.) -- **`deprecation`**: Signifying a newly deprecated feature, scheduled for eventual removal. -- **`internal`** Fully internal change that doesn't affect the public API, but is significant enough to be mentioned, - likely because it affects project contributors. (Such as a new linter rule, change in code style, significant change - in internal API, ...) - -For changes that do not fall under any of the above cases, please specify the lack of the changelog in the pull request -description, so that a maintainer can skip the job that checks for presence of this fragment file. - -## Create fragments with commands - -While you absolutely can simply create these files manually, it's a much better idea to use the `towncrier` library, -which can create the file for you in the proper place. With it, you can simply run `towncrier create -{pull_request}.{type}.md` after creating the pull request, edit the created file and commit the changes. - -If the change is simple enough, you can even use the `-c`/`--content` flag and specify it directly, like: `towncrier -create 12.feature.md -c "Add dinosaurs!"`, or if you're used to terminal editors, there's also the `--edit` flag, which -opens the file with your `$EDITOR`. - -## Preview changelog - -To preview the latest changelog, run `towncrier build --draft --version [version number]`. (For version number, you can -pretty much enter anything as this is just for a draft version. For true builds, this would be the next version number, -so for example, if the current version is 1.0.2, next one will be one either 1.0.3, or 1.1.0, or 2.0.0. But for drafts, -you can also just enter something like `next` for the version, as it's just for your own private preview.) - -To make this a bit easier, there is a taskipy task running the command above, so you can just use `poetry run task -changelog-preview` to see the changelog, if you don't like remembering new commands. - -## Multiple fragments in single PR - -If necessary, multiple fragment files can be created per pull-request, with different change types, if the PR covers -multiple areas. For example for PR #13 that both introduces a feature, and changes the documentation, can add -2 fragment files: `13.feature.md` and `13.docs.md`. - -Additionally, if a single PR is addressing multiple unrelated topics in the same category, and needs to make multiple -distinct changelog entries, an optional counter value can be added at the end of the file name (needs to be an -integer). So for example PR #25 which makes 2 distinct internal changes can add these fragment files: -`25.internal.1.md` and `25.internal.2.md`. (The numbers in this counter position will not be shown in the final -changelog and are merely here for separation of the individual fragments.) - -However if the changes are related to some bigger overarching goal, you can also just use a single fragment file with -the following format: - -```markdown -Update changelog -- Rename `documentation` category to shorter: `docs` -- Add `internal` category for changes unrelated to public API, but potentially relevant to contributors -- Add github workflow enforcing presence of a new changelog fragment file for each PR - - For insignificant PRs which don't require any changelog entry, a maintainer can add `skip-fragment-check` label. -``` - -That said, if you end up making multiple distinct changelog fragments like this, it's a sign that your PR is probably -too big, and you should split it up into multiple PRs instead. Making huge PRs that address several unrelated topics at -once is generally a bad practice, and should be avoided. If you go overboard, your PR might even end up getting closed -for being too big, and you'll be required to split it up. - -## Footnotes - -- See for more info about why and how to properly maintain a changelog -- For more info about `towncrier`, check out it's [documentation](https://towncrier.readthedocs.io/en/latest/tutorial.html) +To learn more about changelog fragments, see our [documentation](https://py-mine.github.io/mcproto/latest/installation/changelog/) diff --git a/docs/contributing/guides/changelog.md b/docs/contributing/guides/changelog.md index fb61abca..a8d51360 100644 --- a/docs/contributing/guides/changelog.md +++ b/docs/contributing/guides/changelog.md @@ -1,3 +1,256 @@ -!!! bug "Work In Progress" +# Changelog fragments - This page is still being written. The content below (if any) may change. +???+ abstract + + This page describes our use of `towncrier` for the project's changelog. It explains the different changelog + categories, the process of creating changelog fragment files and generating a changelog preview. Additionally, the + page contains a guide on writing good changelog fragments. + +Our project contains a changelog which tracks all notable changes for easy and quick reference to both users and our +contributors. + +To maintain our changelog, we're using [`towncrier`](https://towncrier.readthedocs.io/en/stable/), which allows us to +create **fragment files**, which each contains a single changelog entry. Once a new release is created, all of +these fragments will be used to create a changelog for that new release. + +We generally require every pull request to to include a new changelog fragment, summarizing what it does. + +!!! note + + If you think your change shouldn't require a changelog entry (it's a small / simple change that isn't worth + noting), ask us to add the `skip-fragment-check` label to your PR, which will disable the automated check that + enforces a presence of the changelog fragment. + +## Structure of a fragment file + +The fragment files are stored in the `changes/` directory in our project. These files follow the following naming +format: `{pull_request_number}.{type}.md`. + +Possible fragment types are: + +- **`feature`**: New feature that affects the public API. +- **`bugfix`**: A bugfix, which was affecting the public API. +- **`docs`**: Change to the documentation, or updates to public facing docstrings +- **`breaking`**: Signifying a breaking change of some part of the project's public API, which could cause issues for + end-users updating to this version. (Includes deprecation removals.) +- **`deprecation`**: Signifying a newly deprecated feature, scheduled for eventual removal. +- **`internal`** Fully internal change that doesn't affect the public API, but is significant enough to be mentioned, + likely because it affects project contributors. (Such as a new linter rule, change in code style, significant change + in internal API, ...) + +## Create fragments with commands + +While you can absolutely create these files manually, it's often a lot more convenient to use the `towncrier` CLI, +which can create the file for you in the proper place automatically. With it, you can simply run: + +```bash +towncrier create {pull_request_number}.{type}.md +``` + +After you ran the command, a new file will appear in the `changes/` directory. You can now open it and describe your +change inside of it. + +If the change is simple enough, you can even use the `-c` / `--content` flag to specify it directly, like: + +```bash +towncrier create 12.feature.md -c "Add dinosaurs!" +``` + +!!! tip "Terminal editors" + + If you're used to terminal editors, there's also an `--edit` flag, which will open the file with your + `$EDITOR`. (I would recommend `neovim`, but if you find it too complex, `nano` also works well) + +## Multiple fragments in a single PR + +If necessary, multiple fragment files can be created per pull-request, with different change types, if the PR covers +multiple areas. For example for PR #13 that both introduces a feature, and changes the documentation, can add 2 +fragment files: `13.feature.md` and `13.docs.md`. + +Additionally, if a single PR is addressing multiple unrelated topics in the same category, and needs to make multiple +distinct changelog entries, an optional counter value can be added at the end of the file name (needs to be an +integer). So for example PR #25 which makes 2 distinct internal changes can add these fragment files: +`25.internal.1.md` and `25.internal.2.md`. (The numbers in this counter position will not be shown in the final +changelog and are merely here for separation of the individual fragments.) + +However if the changes are related to some bigger overarching goal, you can also just use a single fragment file with +the following format: + +```markdown title="changes/25.internal.md" +Update towncrier categories + + - Rename `documentation` category to shorter: `docs` + - Add `internal` category for changes unrelated to public API, but potentially relevant to contributors + - Add github workflow enforcing presence of a new changelog fragment file for each PR + - For insignificant PRs which don't require any changelog entry, a maintainer can add `skip-fragment-check` label. +``` + +!!! warning + + While possible, if you end up making multiple distinct changelog fragments like this, it's a sign that your PR + might be too big, and you should split it up into multiple PRs instead. Making huge PRs that address several + unrelated topics at once is generally a bad practice, and should be avoided. If you go overboard, your PR might + even end up getting closed for being too big, and you'll be required to split it up. + +## Preview changelog + +To preview the latest changelog, run `towncrier build --draft --version latest`. + +??? note "Meaning of the version value" + + The `--version` attribute usually takes the version number of the project, to which these changes apply. However, + since we just want to preview the changes, it doesn't really matter for us, so we can just pass `latest` or + whatever else you wish. + + For actual builds, the version is automatically obtained and this command is executed in our release CI workflow. + + This version will be used in the first line of the changelog (the header). + +??? note "Meaning of --draft flag" + + The `--draft` flag will make sure that towncrier will only show you the contents of the next changelog version + entry, but won't actually add that generated content to our `CHANGELOG.md` file, while consuming (removing) the + changelog fragments. + + You will never need to run `towncrier` without the `--draft` flag, as our CI workflows for project releasing handle + that automatically. + +To make this a bit easier, there is a taskipy task running the command above, so you can just use `poetry run task +changelog-preview` to see the changelog, if you don't like remembering new commands. + +## Writing good changelog fragments + +Fragment files follow the same markdown syntax as our documentation. + +The contents of a fragment file should describe the change that you've made in a quick and general way. That said, the +change descriptions can be a bit more verbose than just the PR title, but only if it's necessary. Keep in mind that +these changes will be shown to the end users of the library, so try to explain your change in a way that a +non-contributor would understand. + +!!! tip + + If your change needs some more in-depth explanations, perhaps with code examples and reasoning for why such a + change was made, use the PR body (description) for this purpose. Each changelog entry will contain a link to the + corresponding pull request, so if someone is interested in any additional details about a change, they can always + look there. + +### Examples of good changlog fragment files + +:material-check:{ style="color: #4DB6AC" } **Clear and descriptive** + +```markdown title="changes/171.feature.md" +Add `Account.check` function, to verify that the access token in use is valid, and the data the Account instance has matches the data minecraft API has. +``` + +```markdown title="changes/179.docs.md" +Enforce presence of docstrings everywhere with pydocstyle. This also adds docstring to all functions and classes that didn't already have one. Minor improvements for consistency were also made to some existing docstrings. +``` + +:material-check:{ style="color: #4DB6AC" } **Slightly on the longer side, but it's justified** (Sometimes, it's +important to explain the issue that this fixes, so that users know that it was there) + +```markdown title="changes/330.bugfix.md" +Fix behavior of the `mcproto.utils.deprecation` module, which was incorrectly always using a fallback version, assuming mcproto is at version 0.0.0. This then could've meant that using a deprecated feature that is past the specified deprecation (removal) version still only resulted in a deprecation warning, as opposed to a full runtime error. +``` + +:material-check:{ style="color: #4DB6AC" } **With an extra note about the breaking change** (Adding some extra +description isn't always bad, especially for explaining how a breaking change affects existing code) + +```markdown title="changes/130.breaking.md" +Renamed "shared_key" field to "shared_secret" in `LoginEncryptionPacket`, following the official terminology. + + This is a breaking change as `LoginEncryptionPacket`'s `__init__` method now uses "shared_secret" keyword only + argument, not "shared_key". Every initialization call to this packet needs to be updated. +``` + +:material-check:{ style="color: #4DB6AC" } **With a list of subchanges that were made** (Be careful with this one +though, make sure you don't over-do it) + +```markdown title="changes/129.feature.md" +Added a system for handling Minecraft authentication + + - Yggdrasil system for unmigrated i.e. non-Microsoft accounts (supportng Minecraft accounts, and the really old + Mojang accounts) + - Microsoft OAuth2 system (Xbox live) for migrated i.e. Microsoft accounts +``` + +### Examples of bad changelog fragment files + +:material-close:{ style="color: #EF5350" } **Unclear** (But what does this class do?) + +```markdown title="changes/123.feature.md" +Update `Buffer` class. +``` + +:material-close:{ style="color: #EF5350" } **Bad category** (This is a feature, not a bugfix) + +```markdown title="changes/161.bugfix.md" +Add support for encryption. Connection classes now have `enable_encryption` method, and some encryption related functions were added into a new mcproto.encryption module. +``` + +:material-close:{ style="color: #EF5350" } **Starts with dash** (Sometimes, it can feel natural to start your changelog +entry with a `-`, as it is a list item in the final changelog, however, this dash will already be added automatically) + +```markdown title="changes/171.feature.md" +- Add `Account.check` function, to verify that the access token in use is valid, and the data the Account instance has matches the data minecraft API has. +``` + +:material-close:{ style="color: #EF5350" } **Wrapped first line** (Splitting up the first line into multiple lines is +something we often do in markdown, because it should still be rendered as a single line, however, because of how +towncrier merges these fragments, using multiple lines will cause issues and the changelog won't be formatter +correctly! Further blocks can have wrapped lines.) + +```markdown title="changes/330.bugfix.md" +Fix behavior of the `mcproto.utils.deprecation` module, which was incorrectly always using a fallback version, assuming +mcproto is at version 0.0.0. This then could've meant that using a deprecated feature that is past the specified +deprecation (removal) version still only resulted in a deprecation warning, as opposed to a full runtime error. +``` + +:material-close:{ style="color: #EF5350" } **No indent in description** (Sometimes, we want to add additional +description to our changelog entry. When doing so, we need to make sure that the description block is indented with 4 +spaces and there is a blank line after the first / title line.) + +```markdown title="changes/330.breaking.md" +Renamed "shared_key" field to "shared_secret" in `LoginEncryptionPacket`, following the official terminology. + +This is a breaking change as `LoginEncryptionPacket`'s `__init__` method now uses "shared_secret" keyword only +argument, not "shared_key". Every initialization call to this packet needs to be updated. +``` + +:material-close:{ style="color: #EF5350" } **Way too long** (This should've been the PR description) + +```markdown title="changes/161.feature.md" +Introduce support for encryption handling. + + Most servers (even offline ones) usually send an EncryptionRequest packet during the LOGIN state, with a public + (RSA) key that the client is expected to use to encrypt a randomly generated shared secret, to send back to the + server in EncryptionResponse packet. After that, all further communication is encrypted with this shared secret. + + The encryption used is a AES/CFB8 stream cipher. That means the encrypted ciphertext will have the same amount + of bytes as the original plaintext, allowing us to still trust our reader/writer methods that rely on reading + specific amounts of bytes, even if their content don't make sense. + + This directly uses the base connection classes and adds enable_encryption method to them, which after getting + called will automatically encrypt/decrypt any incomming/outcomming data. + + This additionally also changes the LoginEncryptionRequest packet class, and makes the public key attribute + actually hold an RSA public key (from the cryptography library), instead of just the received bytes. This is + then much more useful to work with later on. This is a breaking change. +``` + +!!! tip "Verify if your changelog works" + + Our CI will automatically build the documentation for your PR and post a link to it as a comment in the pull + request. This documentation will include a preview of the changelog with all unreleased changes in the + [changelog](../../installation/changelog.md) page. You can take a look there to make sure that your change + fragment(s) resulted in the proper output. + +!!! note "Internal changes" + + We're a bit more forgiving when it comes to describing your change if your change is in the `internal` category, as + end users don't need to read those. Changes in this category can be a bit less descriptive. + +## Footnotes + +- See for more info about why and how to properly maintain a changelog +- For more info about `towncrier`, check out it's [documentation](https://towncrier.readthedocs.io/en/latest/tutorial.html) From e499bf75b794061f1830286fecc61e3c745e80eb Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 19:21:17 +0200 Subject: [PATCH 40/85] Fix reference to git-pull --- docs/contributing/guides/setup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/guides/setup.md b/docs/contributing/guides/setup.md index fbd4319d..f48216cc 100644 --- a/docs/contributing/guides/setup.md +++ b/docs/contributing/guides/setup.md @@ -20,8 +20,8 @@ requirement to work on this project. This guide assumes you have already [forked][github-forking] our repository, [clonned it][git-cloning] to your computer and created your own [git branch][git-branches] to work on. -If you wish to work from an already forked repository, make sure to check out the main branch and do a [`git pull`] to -get your fork up to date. Now create your new branch. +If you wish to work from an already forked repository, make sure to check out the main branch and do a [`git +pull`][git-pull] to get your fork up to date. Now create your new branch. [git-and-github]: https://docs.github.com/en/get-started/start-your-journey/about-github-and-git [github-forking]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo From 82dc9c411555c24d498f25a2eb6cc4f0c3987aa8 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 5 Aug 2024 19:29:14 +0200 Subject: [PATCH 41/85] Add some todos --- docs/contributing/guides/setup.md | 2 +- docs/contributing/guides/style-guide.md | 8 ++++++++ docs/contributing/making-a-pr.md | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/contributing/guides/setup.md b/docs/contributing/guides/setup.md index f48216cc..b50f68a3 100644 --- a/docs/contributing/guides/setup.md +++ b/docs/contributing/guides/setup.md @@ -1,6 +1,6 @@ !!! bug "Work In Progress" - This page is still being written. The content below (if any) may change. + This page is missing a guide on configuring vscode to pick up poetry environment. # Setting up the project diff --git a/docs/contributing/guides/style-guide.md b/docs/contributing/guides/style-guide.md index 9ecad330..201715e2 100644 --- a/docs/contributing/guides/style-guide.md +++ b/docs/contributing/guides/style-guide.md @@ -1,3 +1,7 @@ +!!! bug "Work In Progress" + + This page is missing a guide on ruff editor integration + # Style Guide ???+ abstract @@ -113,6 +117,10 @@ To make ruff format your code, simply run: ruff format . ``` +### Editor integration + +TODO + ## Other style guidelines While `ruff` can do a lot, it can't do everything. There are still some guidelines that you will need to read over and diff --git a/docs/contributing/making-a-pr.md b/docs/contributing/making-a-pr.md index 16cd6313..bf2353d0 100644 --- a/docs/contributing/making-a-pr.md +++ b/docs/contributing/making-a-pr.md @@ -1,3 +1,7 @@ +!!! bug "Work In Progress" + + This page is missing a guide on writing a good PR body + # Pull Requests Welcome! If you're interested in contributing to mcproto, you've come to the right place. mcproto is an open-source @@ -45,6 +49,10 @@ being used, which would be a shame. There's therefore no point in cluttering the issue tracker with a bunch of small issues that can often be changed in just a few minutes. +## Pull Request Body + +TODO + ## Work in Progress PRs Whenever you open a pull request that isn't yet ready to be reviewed and merged, you can mark it as a **draft**. This From 48f442545bd1fecf4d8cef4a1cf922e74c682271 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 6 Aug 2024 17:36:32 +0200 Subject: [PATCH 42/85] Fix some typos in the docs --- docs/contributing/making-a-pr.md | 2 +- docs/contributing/reporting-a-bug.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/making-a-pr.md b/docs/contributing/making-a-pr.md index bf2353d0..a19cf1aa 100644 --- a/docs/contributing/making-a-pr.md +++ b/docs/contributing/making-a-pr.md @@ -4,7 +4,7 @@ # Pull Requests -Welcome! If you're interested in contributing to mcproto, you've come to the right place. mcproto is an open-source +Welcome! If you're interested in contributing to mcproto, you've come to the right place. Mcproto is an open-source project, and we welcome contributions from anyone eager to help out. To contribute, you can create a [pull request](https://docs.github.com/en/pull-requests) on our GitHub repository. diff --git a/docs/contributing/reporting-a-bug.md b/docs/contributing/reporting-a-bug.md index b7c6c1fb..068b50b1 100644 --- a/docs/contributing/reporting-a-bug.md +++ b/docs/contributing/reporting-a-bug.md @@ -1,6 +1,6 @@ # Bug reports -Mcproto is an actively maintained project that we constantly strive to improve. With a project of this siez and +Mcproto is an actively maintained project that we constantly strive to improve. With a project of this size and complexity, bugs may occur. If you think you have discovered a bug, you can help us by submitting an issue to our public [issue tracker](https://github.com/py-mine/mcproto/issues), following this guide. From 314b15f7378e29c69e88262e27063519baed6abd Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 6 Aug 2024 20:12:32 +0200 Subject: [PATCH 43/85] License the docs itself under CC BY-NC-SA 4.0 --- docs/LICENSE.md | 23 +++++++++++++++++++++++ docs/community/license.md | 30 ++++++++++++++++++++++++++++-- mkdocs.yml | 6 +++++- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 docs/LICENSE.md diff --git a/docs/LICENSE.md b/docs/LICENSE.md new file mode 100644 index 00000000..ed1add7a --- /dev/null +++ b/docs/LICENSE.md @@ -0,0 +1,23 @@ +# Documentation License + +This documentation itself does NOT follow the primary project license! + +Instead, it follows a Creative Commons license:
CC BY-NC-SA 4.0 + +## Attribution + +If you need a copyright header for proper attribution, you can use: + +Mcproto Documentation © 2024 by ItsDrike + +In HTML: + +```html +Mcproto Documentation © 2024 by ItsDrike +``` + +If you also need the license identifier, use the following: + +```html +CC BY-NC-SA 4.0 +``` diff --git a/docs/community/license.md b/docs/community/license.md index 971e2a34..d18e3951 100644 --- a/docs/community/license.md +++ b/docs/community/license.md @@ -1,6 +1,6 @@ # License -This project is licensed under the **GNU Lesser General Public License** (LGPL) version 3. +This project's source code is licensed under the **GNU Lesser General Public License** (LGPL) version 3. The LGPL license allows you to use mcproto as a library pretty much in any code-base, including in proprietary code-bases. However, if you wish to make a derivative project to mcproto itself, such a project will need to be licensed under @@ -12,8 +12,34 @@ LGPL as well. --8<-- "LICENSE.txt" ``` +## This documentation + +This documentation itself follows a Creative Commons license: CC BY-NC-SA 4.0 + +!!! tip + + If you need a copyright header for proper attribution, you can use: + + === "Rendered" + + Mcproto Documentation © 2024 by ItsDrike + + === "HTML" + + ```html + Mcproto Documentation © 2024 by ItsDrike + ``` + + If you also need the license identifier, use the following: + + ```html + CC BY-NC-SA 4.0 + ``` + +## Differently licensed parts + Some parts of the project follow a different license. See the `LICENSE-THIRD-PARTY.txt` file, which lists all of these -parts and their respective licenses +parts and their respective licenses. ??? example "Full LICENSE-THIRD-PARTY text" diff --git a/mkdocs.yml b/mkdocs.yml index ea0ae5a3..5521e103 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,11 @@ site_url: https://py-mine.github.io/mcproto repo_url: https://github.com/py-mine/mcproto repo_name: py-mine/mcproto -watch: [LICENSE.txt, LICENSE-THIRD-PARTY.txt, ATTRIBUTION.md, CHANGELOG.md, changes] +watch: + [LICENSE.txt, LICENSE-THIRD-PARTY.txt, ATTRIBUTION.md, CHANGELOG.md, changes] + +exclude_docs: | + LICENSE.md nav: - Home: index.md From 866b0c6d943f0ebde551dcafbad3d6d07bf4ca1f Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 7 Aug 2024 12:28:39 +0200 Subject: [PATCH 44/85] Add great commits guide --- docs/contributing/guides/great-commits.md | 326 +++++++++++++++++++++- 1 file changed, 324 insertions(+), 2 deletions(-) diff --git a/docs/contributing/guides/great-commits.md b/docs/contributing/guides/great-commits.md index fb61abca..3442e3ba 100644 --- a/docs/contributing/guides/great-commits.md +++ b/docs/contributing/guides/great-commits.md @@ -1,3 +1,325 @@ -!!! bug "Work In Progress" +# Great Commits - This page is still being written. The content below (if any) may change. +???+ abstract + + This guide describes how to make good commits that are helpful to maintainers, debuggable and readable when going + over the `git log`, or `git blame`. + + It explains the purpose of a commit message and it's structure, goes over the importance of making commits + "atomic" and the practice of partial staging, mentions why and how to avoid making a lot of fixing commits, + describes force pushing after modifying the git history, alongside it's downsides and finally, it explains why + these practices are worth following and how they make the developer's life easier. + +A well-structured git log is crucial for a project's maintainability, providing insight into changes as a reference for +future maintainers (or old forgetful ones, _like me_). Here, we outline the best practices for making good commits in +our project. + +## Commit Message Guidelines + +### Purpose + +Every commit should represent a change in the source code. The commit message should not only describe **what** was +changed but also **why** it was necessary and what it achieves. + +### More than just the first line + +Many developers are uesd to commiting changes with a simple `git commit -m "My message"`, and while this is enough and +it's perfectly fine in many cases, sometimes you just need more space to describe what a change truly achieves. + +Surprisingly, many people don't even know that they can make a commit that has more in it's message than just the +title/first line. That then leads to poorly documented changes, because single line sometimes just isn't enough. + +To create a commit with a bigger commit message, you can simply run the `git commit` command without the `-m` argument. +This should open a temporary file in your text editor (`$EDITOR`), in which you can write out your commit message in +full. + +??? tip "Use git commit by default" + + I’d actually recommend making the simple `git commit` the default way you make new commits, since it invites you to + write more about it, by just seeing that you have that space available. We usually don’t even know what exactly + we’ll write in our new commit message before getting to typing it out, and knowing you have that extra space if you + need it will naturally lead to using it, even if you didn’t know you needed it ahead of time. + +!!! note + + That said, not every commit requires both a subject and a body. Sometimes, a change may be so simple, that no + further context is necessary. With those changes, including a body would just be a waste of the readers time. For + example: + + ```markdown + Fix typo in README + ``` + + This message doesn't need anything extra. Some people like to include what the typo was, but if you want to know + that, you can just look at the actual changes that commit made. There's a whole bunch of ways to do that with git, + like `git show`, `git diff` or `git log --patch`. So while in some cases, having extra context can be very + valuable, you also shouldn't overdo it. + +### Structure + +Git commits should be written in a very specific way. There’s a few rules to follow: + +1. **Subject Line:** + - **Limit to 50 characters** (This isn't a hard limit, but try not to go much longer. This limit ensures + readability and forces the author to thing about the most concise way to explain what's going on. Hint: If you're + having trouble summarizing, you might be committing too much at once) + - **A single sentence** (The summary should be a single sentence, multiple probably wouldn't fit into the character + limit anyways) + - **Capitalize the first letter** + - **Don't end with a period** (A period will only waste one of your precious 50 characters for the summary and + it's not very useful context wise) + - **Use imperative mood** (Imperative mood means “written as if giving a command/instruction” i.e.: “Add support + for X”, not “I added support for X” or “Support for X was added”, as a rule of thumb, a subject message should be + able to complete the sentence: “If implemented, this commit will …”) +2. **Body:** + - **Separate the body from the subject line with a blank line** (Not doing so would make git think your summary + spans across multiple lines, rather than it being a body) + - **Wrap at 72 characters** (Commits are often printed into the terminal with the `git log` command. If the output + isn't wrapped, going over the terminals width can cause a pretty messy output. The recommended maximum width for terminal text output is 80 characters, but git tools can often add indents, so 72 characters is a sensible maximum) + - **Avoid implementation details** (The diff shows the "how", focus on the "what" and "why") + +Git commits can use markdown, most other programs will understand it and it's a great way to bring in some more +style, improving the readability. In fact, if you view the commit from a site like GitHub, it will automatically +render any markdown in the commit for you. + +???+ example "Example commit" + + ```markdown + Summarize changes in around 50 characters or less + + More detailed explanatory text, if necessary. Wrap it to about 72 + characters or so. In some contexts, the first line is treated as the + subject of the commit and the rest of the text as the body. The + blank line separating the summary from the body is critical (unless + you omit the body entirely); various tools like `log`, `shortlog` + and `rebase` can get confused if you run the two together. + + Explain the problem that this commit is solving. Focus on why you + are making this change as opposed to how (the code explains that). + Are there side effects or other unintuitive consequences of this + change? Here's the place to explain them. + + Further paragraphs come after blank lines. + + - Bullet points are okay too + - They're very useful for listing something + ``` + +:material-run-fast: **Stretch goal** – Include relevant **keywords** to make your commits easily searchable (e.g. the +name of the class/function you modified). + +:material-run-fast: **Stretch goal \#2** – Keep it **engaging**! Provide some interesting context or debug processes to +make the commit history both more informative and fun to read. + +## Make "atomic" commits + +!!! quote "Definition" + + *Atomic: of or forming a single irreducible unit or component in a larger system.* + +The term “atomic commit” means that the commit is only representing a single change, that can’t be further reduced into +multiple commits, i.e. this commit only handles a single change. Ideally, it should be possible to sum up the changes +that a good commit makes in a single sentence. + +That said, the irreducibility should only apply to the change itself, obviously, making a commit for every line of code +wouldn’t be very clean. Having a commit only change a small amount of code isn’t what makes it atomic. While the commit +certainly can be small, it can just as well be a commit that’s changing thousands of lines. (That said, you should have +some really good justification for it if you’re actually making commits that big.) + +The important thing is that the commit is only responsible for addressing a single change. A counter-example would be a +commit that adds a new feature, but also fixes a bug you found while implementing this feature, and also improves the +formatting of some other function, that you encountered along the way. With atomic commits, all of these actions would +get their own standalone commits, as they’re unrelated to each other, and describe several different changes. + +Note that making atomic commits isn't just about splitting thins up to only represent single changes, indeed, while +they should only represent the smallest possible change, it should also be a “complete” change. This means that a +commit responsible for changing how some function works in order to improve performance should ideally also update the +documentation, make the necessary adjustments to unit-tests so they still pass, and update all of the references to +this updated function to work properly after this change. + +!!! abstract "Summary" + + So, an atomic commit is a commit representing a single (ideally an irreducible) change, that’s fully implemented + and integrates well with the rest of the codebase. + +### Partial adds + +Many people tend to always simply use `git add -A` (or `git add .`), to stage all of the changes they made, and then +create a commit with it all. Sometimes, you might not even stage the changes and choose to use `git commit -a`, to +quickly commit everything. + +In an ideal world, where you only made the changes you needed to make for this single atomic commit, this would work +pretty well, and while sometimes this is the case, in many cases, you might've also fixed a bug or a typo that you +noticed while working on your changes, or already implemented something else, that doesn't fit into your single atomic +commit that you now wish to make. + +In this case, it can be very useful to know that you can instead make a "partial" add, and only stage those changes +that belong to the commit. In some cases, all that you'll need is to only stage some specific files, which you can do +with: + +```bash +git add path/to/some/file path/to/other/file +``` + +That said, in most cases, you're left with a single file that contains multiple changes. When this happens, you can use +the `-p`/`--patch` flag: + +```bash +git add -p path/to/file +``` + +Git will then let you interactively go over every "hunk" (a chunk of code, with changes close to each other) and let +you decide whether to accept it (hence staging that single hunk), split it into more chunks, skip it (avoids staging +this hunk) or even modify it in your editor, allowing you to remove the intertwined code from multiple changes, so that +your commit will really only perform a single change. + +!!! tip "Use --patch more often" + + This git feature has slowly became one of my favorite tools, and I use it almost every time I need to commit + something, even if I don't need to change or skip things, since it also allows me to quickly review the changes + I'm making, before they make it into a commit. + +## Avoid fixing commits + +A very common occurrence I see in a ton of different projects is people making sequences of commits that go like: + +- Fix bug X +- Actually fix bug X +- Fix typo in variable name +- Sort imports +- Follow lint rules +- Run auto-formatter + +While people can obviously mess up sometimes, and just not get something right on the first try, a fixing commit is +rarely a good way to solve that. + +Instead of making a new commit, you can actually just amend the original. To do this, we can use the `git commit +--amned`, which will add your staged changes into the previous commit, even allowing you to change the message of that +old commit. + +Not only that, if you've already made another commit, but now found something that needs changing in the commit before +that, you can use interactive rebase with `git rebase -i HEAD~3`, allowing you to change the last 3 commits, or even +completely remove some of those commits. + +For more on history rewriting, I'd recommend checking the [official git +documentation](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History). + +### Force pushing + +Changing history is a great tool to clean up after yourself, but it works best with local changes, i.e. with changes +you haven't yet pushed. + +If you're changing git history after you've already pushed, you will find that pushing again will not work, giving you +a message like "updates were rejected because the remote contains work that you do not have locally". + +To resolve this issue, it is possible to make a "force push" with `git push --force` command. Running this will push +your branch to the remote (to GitHub) regardless of what was in the remote already, hence overriding it. + +!!! warning + + Force pushing becomes risky if others have already pulled the branch you are working on. If you overwrite the + branch with a force push, it can lead to several issues: + + - **Lost Work:** Collaborators may have pushed to your branch already, following it's existing git history. + However, after your force-push, their changes would be ereased from the remote. **Make sure you pull / rebase + from the remote before you make a force-push.** + - **Complex conflicts:** If someone else has pulled your branch and did some changes that they didn't yet push + before you force-pushed, suddenly, their git history is now no longer in sync. Resolving conflicts like that is + possible, but it can be very annoying. + - **Harder reviews:** When reviewing your code, we sometimes like going over the individual commits to understand + your individual (atomic) changes better. It's often a lot easier to look at and review 10 different atomic + changes individually, that together form a PR than it would be to look at all of them at once. By force-pushing, + you're changing the commit history, making the changes to the code that we already reviewed. This is partially + GitHub's fault though, for not providing an easier way of showing these changes across force-pushes. + +#### Force pushing on PR feature branches + +In our project, we do allow force pushing on your individual feature branches that you use for your PR. This +flexibility enables you to clean up your commit history and refine your changes before they are merged into the main +branch. However, it's important to note that many other projects may not permit force pushing due to the risks +involved. Always check the contributing guidelines of the project you are working on. + +!!! tip "Best practices" + + To mitigate the risks associated with force pushing, consider following these best practices: + + - **Push less often:** Try to limit of othen you push changes to the remote repository in general. Aim to push only + when you are satisfied with the set of changes you have. This reduces the likelihood of needing to force-push a + lot. + - **Force push quickly:** If you do need to force-push, try to do so as quickly as possible. The more time that has + passed since your normal push, the more likely it is that someone have already clonned/pulled your changes. If a + force push was made within just a few seconds of the original push (and it only overwrites the changes from that + last push), it's not very likely that someone will have those changes pulled already, so you probably won't break + anyone's local version. + - **Pull before changing history:** Make absolutely certain that you don't override anyone's changes with your + force-push. Sometimes, maintainers can create new commits in your branch, other times, that can even be you by + modifying something from GitHub, or clicking on the apply suggestion button from a code-review. By pulling before + you start changing history, you can make sure that you won't erease these changes and they'll remain a part of + your modified history. + +## Benefits + +Now that you've seen some of the best practices to follow when making new commits, let's talk a bit about why we follow +these practices and what benefits we can gain from them. + +### A generally improved development workflow + +Speaking from my personal experience, I can confidently say that learning how to make good git commits, specifically +the practice of making atomic commits will make you a better programmer overall. That might sound surprising, but it's +really true. + +The reason is that it forces you to only tackle one issue at a time. This naturally helps you to think about how to +split your problem into several smaller (atomic) subproblems and make commits addressing those single parts. This is +actually one of very well known approaches to problem-solving, called the "divide and conquer" method, where you split +your problem into really small, trivially simple chunks that you solve one by one. + +### Easier bug hunting + +Bugs in code are pretty much inevitable, even for the most experienced of developers. Sometimes, we just don't realise +how certain part of the code-base will interact with another part, or we're just careless as we try and build something +fast. + +The most annoying bugs are those that aren't discovered immediately during development. These bugs can require a lot of +work to track down. With a good git log, filled with a lot of small commits, where each commit leaves the code-base in +a usable state, you can make this process a lot simpler! + +Git has a command specifically for this: `git bisect`. It will first make you mark 2 commits, a good one and a bad one, +after which it will perform a binary search, checking out the commits in between these two as you try and replicate the +bug on each. This will quickly lead you to the specific commit that introduced this bug, without having to do any code +debugging at all. + +The great advantage here is that users reporting bugs can often perform git bisects too, even without having to know +much about development and the structure of our code-base and if the identified commit is small enough, the issue is +often apparent just from looking at the diff. Even for bigger commits though, they can be often reverted to quickly fix +the issue and give developers time to focus on actually resolving it, while using it's diff as a reference. + +### Enhanced git blame + +Clear commit messages can be very useful for understanding certain parts of the code. Git provides a tool called `git +blame`, which can show you which commit is responsible for adding a specific line into the code-base. From there, you +can then take a look at that commit specifically and see it's title & description to further understand that change, +along with the rest of the diff to give you proper context for how that line worked with the rest of the code. + +This can often be a great tool when refactoring, as sometimes it can be quite unclear why something is done the way it +is and commits can sometimes help explain that. + +### Efficient cherry picking + +In some cases, it can be useful to carry over certain change (commit) from one place to another. This process is called +cherry-picking (`git cherry-pick`), which will copy a commit and apply it's diff elsewhere. With atomic commits, this +will often work without any further adjustments, since each commit should itself leave you with a functioning project. + +### Streamlined pull request reviews + +Reviewers can often better understand and verify changes by examining your well-structured commits, improving the +review process. + +## Footnotes + +This guide took **heavy** inspiration from this article: . + +!!! quote + + P.S. It's not plagiarism if the original was written by me :P + +See the original article's sources for proper attributions. From 444fbee6363a9c96188c433f9f7ccf8537a2e531 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 8 Aug 2024 23:30:55 +0200 Subject: [PATCH 45/85] Remove license from coc (docs now have global cc license) --- docs/community/code-of-conduct.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/community/code-of-conduct.md b/docs/community/code-of-conduct.md index a5d1fe0c..1550b780 100644 --- a/docs/community/code-of-conduct.md +++ b/docs/community/code-of-conduct.md @@ -147,9 +147,3 @@ list these projects: - Rust-lang: - Code Fellows: - Python Discord: - -## License - -All content of this page is licensed under a Creative Commons Attributions license. - -For more information about this license, see: From 0f2dbd33742bf0ac4c62b2c9c37432e085b8796f Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 8 Aug 2024 23:39:10 +0200 Subject: [PATCH 46/85] Add tip about skipping internal changelog changes --- docs/installation/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/installation/changelog.md b/docs/installation/changelog.md index 83255362..5599c918 100644 --- a/docs/installation/changelog.md +++ b/docs/installation/changelog.md @@ -4,6 +4,10 @@ Major and minor releases also include the changes specified in prior development releases. +!!! tip + + Feel free to skip the **Internal Changes** category if you aren't a contributor / core developer of mcproto. + ```python exec="yes" --8<-- "docs/scripts/gen_changelog.py" ``` From 75e8c32e5cc71e79ff02a62c8f1c6e51d02c99b8 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 9 Aug 2024 01:49:02 +0200 Subject: [PATCH 47/85] Add section for running basedpyright from cli --- docs/contributing/guides/type-hints.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/contributing/guides/type-hints.md b/docs/contributing/guides/type-hints.md index db421628..168c7887 100644 --- a/docs/contributing/guides/type-hints.md +++ b/docs/contributing/guides/type-hints.md @@ -90,6 +90,19 @@ The type checker that we use in our code-base is [**basedpyright**](https://docs pyright which adds some extra checks and features and focuses more on the open-source community, than the official Microsoft owned Pyright. +### Running BasedPyright + +To run BasedPyright on the code-base, you can use the following command: + +```bash +basedpyright . +``` + +!!! note "" + + You will need to run this from an [activated](./setup.md#activating-the-environment) poetry environment while + in the project's root directory. + ### Editor Integration === "VSCode" From beb6d32e20a873af2b753ba284e570e2ef0f0d9a Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 9 Aug 2024 02:28:33 +0200 Subject: [PATCH 48/85] Add pre-commit docs --- docs/contributing/guides/precommit.md | 108 +++++++++++++++++++++++++- mkdocs.yml | 2 +- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/docs/contributing/guides/precommit.md b/docs/contributing/guides/precommit.md index fb61abca..2ffaebfd 100644 --- a/docs/contributing/guides/precommit.md +++ b/docs/contributing/guides/precommit.md @@ -1,3 +1,107 @@ -!!! bug "Work In Progress" +# Pre-commit - This page is still being written. The content below (if any) may change. +???+ abstract + + This guide explains what is pre-commit and how to set it up as a git hook that will run automatically before your + commits. It also describes how to run pre-commit manually from the CLI, how to skip some or all of the individual + checks it performs, what happens when hooks edit files and where it's configuration file is. + +Now that you've seen the linters, formatters, type-checkers and other tools that we use in the project, you might be +wondering whether you're really expected to run all of those commands manually, after each change. And of course, no, +you're not, that would be really annoying, and you'd probably also often just forget to do that. + +So, instead of that, we use a tool called [`pre-commit`](https://pre-commit.com/), which creates a [git +hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), that will automatically run before each commit you +make. That means each time when you make a commit, all of these tools will run over the code you updated, and if any of +these linters detects an issue, the commit will be aborted, and you will see which linter failed, and it's output +telling you why. + +## Installing pre-commit + +To install pre-commit as a git hook all you need to do is to run: + +```bash +pre-commit install +``` + +This will install pre-commit as a git hook into your git repository, which will mean it will run automatically before +every new commit you make. + +!!! warning + + Pre-commit itself will be installed via poetry, which means you will need to have an + [activated](./setup.md#activating-the-environment) poetry environment whenever you make a new commit, otherwise, + the pre-commit git hook will fail with command not found. + +## Hooks that modify files + +Sometimes, hooks can end up modifying your files, for example the ruff format hook may do so if your file wasn't +already formatted by ruff. When this happens, the hook itself will fail, which will make git abort the commit. At this +point, you will be left with the original changes still staged, but some files may have been modified, which means +you'll want to `git add` those again, staging these automatic modifications and then make the commit again. + +Note that in case you were only committing a [partial change](./great-commits.md#partial-adds), which means you still +had some parts of the file unstaged, pre-commit will not modify the files for you. Instead, the hook will just fail, +leaving the rest up to you. You should now run the formatter yourself and perform another partial add, updating the +staged changes to be compliant. + +## Running manually + +Even though in most cases, it will be more than enough to have pre-commit run automatically as a git hook, +sometimes, you may want to run it manually without making a commit. + +!!! tip + + You can run this command without having pre-commit installed as a git hook at all. This makes it possible to avoid + installing pre-commit and instead running all checks manually each time. That said, we heavily recommend that you + instead install pre-commit properly, as it's very easy to forget to run these checks. + +To run pre-commit manually you can use the following command: + +```bash +pre-commit run --all-files +``` + +Using this command will make pre-commit run on all files within the project, rather than just running against the +git staged ones, which is the behavior of the automatically ran hook. + +## Skipping pre-commit + +!!! info "Automatic skipping" + + Pre-commit is pretty smart and will skip running certain tools depending on which files you modified. For example + some hooks only check the validity of Python code, so if you haven't modified any Python files, there is no need to + run those hooks. + +Even though in most cases enforcing linting before each commit is what we want, there are some situations where we need +to commit some code which doesn't pass these checks. This can happen for example after a merge, or as a result of +making a single purpose small commit without yet worrying about linters. In these cases, you can use the `--no-verify` +flag when making a commit, telling git to skip the pre-commit hooks and commit normally. When making a commit, this +would look like: + +```bash +git commit -m "My unchecked commit" --no-verify +``` + +You can also only skip a specific hook, by setting `SKIP` environmental variable (e.g. `SKIP=basedpyright`) or even +multiple hooks (`SKIP=ruff-linter,ruff-formatter,slotscheck`). When making a commit, this would look like: + +```bash +SKIP="check-toml,slotscheck,basedpyright" git commit -m "My partially checked commit" +``` + +!!! note "" + + The names of the individual hooks are their ids, you can find those in the [configuration file](#configuration) for + pre-commit. + +!!! warning + + This kind of verification skipping should be used sparingly. We value a clean history which consistently follows + our linting guidelines, and making commits with linting issues only leads to more commits, fixing those issues later. + +## Configuration + +You can find pre-commit's configuration the `.pre-commit-config.yaml` file, where we define which tools should be ran +and how. Currently, pre-commit runs ruff linter, ruff formatter, slotscheck and basedpyright, but also a checker for +some issues in TOML/YAML files. diff --git a/mkdocs.yml b/mkdocs.yml index 5521e103..f5f7818f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,8 +30,8 @@ nav: - Style Guide: contributing/guides/style-guide.md - Docstring formatting: contributing/guides/docstrings.md - Type hinting: contributing/guides/type-hints.md - - Pre-commit: contributing/guides/precommit.md - Other tools: contributing/guides/other-tools.md + - Pre-commit: contributing/guides/precommit.md - Changelog: contributing/guides/changelog.md - Great commits: contributing/guides/great-commits.md - Unit Tests: contributing/guides/unit-tests.md From 762bfa5e385bd72abc6ea002c89cec17a051a63f Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 9 Aug 2024 11:20:37 +0200 Subject: [PATCH 49/85] Add slotscheck page --- docs/contributing/guides/other-tools.md | 5 -- docs/contributing/guides/slotscheck.md | 69 +++++++++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 70 insertions(+), 6 deletions(-) delete mode 100644 docs/contributing/guides/other-tools.md create mode 100644 docs/contributing/guides/slotscheck.md diff --git a/docs/contributing/guides/other-tools.md b/docs/contributing/guides/other-tools.md deleted file mode 100644 index 05643db8..00000000 --- a/docs/contributing/guides/other-tools.md +++ /dev/null @@ -1,5 +0,0 @@ -!!! bug "Work In Progress" - - This page is still being written. The content below (if any) may change. - - diff --git a/docs/contributing/guides/slotscheck.md b/docs/contributing/guides/slotscheck.md new file mode 100644 index 00000000..6e4ec16f --- /dev/null +++ b/docs/contributing/guides/slotscheck.md @@ -0,0 +1,69 @@ +# Slotscheck + +???+ abstract + + This page explains how we enforce the proper use of `__slots__` on our classes with `slotscheck` tool. We go over + what slotted classes, what slotscheck enforces, how to run slotscheck and how to configure it. + +On top of the tools you already saw (ruff & basedpyright), we also have one more tool that performs static analysis on +our code: [**slotscheck**](https://slotscheck.readthedocs.io/en/latest/). + +## What is slotscheck + +Slotscheck is a tool that focuses on enforcing proper use of `__slots__` on classes. + +???+ question "What are slotted classes" + + If you aren't familiar with slotted classes, you should check the [official + documentation](https://wiki.python.org/moin/UsingSlots). That said, if you just want a quick overview: + + - Slots allow you to explicitly declare all member attributes of a class (e.g. declaring `__slots__ = ("a", "b")` + will make the class instances only contain variables `a` and `b`, trying to set any other attribute will result + in an `AttributeError`). + - The reason we like using slots is the efficiency they come with. Slotted classes use up less RAM and offer + a faster attribute access. + + Example of a slotted class: + + ```python + class FooBar: + __slots__ = ("foo", "bar") + + def __init__(self, foo: str, bar: str) -> None: + self.foo = foo + self.bar = bar + + x = FooBar("a", "b") + print(x.a, x.b) + x.c = 5 # AttributeError + ``` + +With a low level project like mcproto, efficiency is important and `__slots__` offer such efficiency at a very low cost +(of simply defining them). + +The purpose of `slotscheck` is to check that our slotted classes are using `__slots__` properly, as sometimes, it is +easy to make mistakes, which result in losing a lot of the efficiency that slots provide. Issues that slotscheck +detects: + +- Detect broken slots inheritance +- Detect overlapping slots +- Detect duplicate slots + +## How to use slotscheck + +To run slotscheck on the codebase, you can use the following command: + +```bash +slotscheck -m mcproto +``` + +!!! note "" + + Make you have an [activated](./setup.md#activating-the-environment) poetry virtual environment and you're in + the project's root directory. + +## Configuring slotscheck + +Sometimes, you may want to ignore certain files from being checked. To do so, you can modify the slotscheck +configuration in `pyproject.toml`, under the `[tool.slotscheck]` option. That said, doing so should be very rare and +you should have a very good reason to ignore your file instead of fixing the underlying issue. diff --git a/mkdocs.yml b/mkdocs.yml index f5f7818f..faf855a6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,7 +30,7 @@ nav: - Style Guide: contributing/guides/style-guide.md - Docstring formatting: contributing/guides/docstrings.md - Type hinting: contributing/guides/type-hints.md - - Other tools: contributing/guides/other-tools.md + - Slotscheck: contributing/guides/slotscheck.md - Pre-commit: contributing/guides/precommit.md - Changelog: contributing/guides/changelog.md - Great commits: contributing/guides/great-commits.md From 7e778fa2185f3dfa7afa26d95ae2206875475a79 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 9 Aug 2024 11:20:59 +0200 Subject: [PATCH 50/85] Update docs taskipy task to use mkdocs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5f422f45..6f6f5b88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -297,7 +297,7 @@ retest = "pytest -v --last-failed" test-nocov = "pytest -v --no-cov --failed-first" retest-nocov = "pytest -v --no-cov --last-failed" changelog-preview = "towncrier build --draft --version next" -docs = "sphinx-build -b dirhtml -d ./docs/_build/doctrees -W -E -T --keep-going ./docs ./docs/_build/html" +docs = "mkdocs serve" [tool.poetry-dynamic-versioning] enable = true From fe9defc57ac5a16f249f1bb8bd78d83ab3f69cae Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 23 Aug 2024 14:42:58 +0200 Subject: [PATCH 51/85] Add link to slotscheck config docs --- docs/contributing/guides/slotscheck.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/contributing/guides/slotscheck.md b/docs/contributing/guides/slotscheck.md index 6e4ec16f..0b80082b 100644 --- a/docs/contributing/guides/slotscheck.md +++ b/docs/contributing/guides/slotscheck.md @@ -64,6 +64,10 @@ slotscheck -m mcproto ## Configuring slotscheck -Sometimes, you may want to ignore certain files from being checked. To do so, you can modify the slotscheck -configuration in `pyproject.toml`, under the `[tool.slotscheck]` option. That said, doing so should be very rare and -you should have a very good reason to ignore your file instead of fixing the underlying issue. +Sometimes, you may want to ignore certain files from being checked. To do so, +you can modify the [slotscheck configuration][slotscheck-config] in +`pyproject.toml`, under the `[tool.slotscheck]` option. That said, doing so +should be very rare and you should have a very good reason to ignore your file +instead of fixing the underlying issue. + +[slotscheck-config]: https://slotscheck.readthedocs.io/en/latest/configuration.html From 0d53599051cefe969606b0f97c2f9fcfc062e7a6 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 18 Oct 2024 06:41:10 +0200 Subject: [PATCH 52/85] Add API reference (with mkdocstrings) --- docs/reference/private/abc.md | 5 ++ docs/reference/private/authentication.md | 13 ++++ docs/reference/private/deprecation.md | 3 + docs/reference/private/index.md | 11 ++++ docs/reference/private/protocol.md | 5 ++ docs/reference/private/types.md | 9 +++ docs/reference/public/authentication.md | 11 ++++ docs/reference/public/encryption.md | 6 ++ docs/reference/public/multiplayer.md | 5 ++ docs/reference/public/packets.md | 35 +++++++++++ docs/reference/public/protocol.md | 9 +++ docs/reference/public/types.md | 5 ++ mkdocs.yml | 42 +++++++++++++ poetry.lock | 76 +++++++++++++++++++++++- pyproject.toml | 3 +- 15 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 docs/reference/private/abc.md create mode 100644 docs/reference/private/authentication.md create mode 100644 docs/reference/private/deprecation.md create mode 100644 docs/reference/private/index.md create mode 100644 docs/reference/private/protocol.md create mode 100644 docs/reference/private/types.md create mode 100644 docs/reference/public/authentication.md create mode 100644 docs/reference/public/encryption.md create mode 100644 docs/reference/public/multiplayer.md create mode 100644 docs/reference/public/packets.md create mode 100644 docs/reference/public/protocol.md create mode 100644 docs/reference/public/types.md diff --git a/docs/reference/private/abc.md b/docs/reference/private/abc.md new file mode 100644 index 00000000..47c67757 --- /dev/null +++ b/docs/reference/private/abc.md @@ -0,0 +1,5 @@ +# Utility Abstract Base Classes + +These are some internal ABC classes that we utilize in various places. + +::: mcproto.utils.abc diff --git a/docs/reference/private/authentication.md b/docs/reference/private/authentication.md new file mode 100644 index 00000000..bd1625d9 --- /dev/null +++ b/docs/reference/private/authentication.md @@ -0,0 +1,13 @@ +# Internal components for authentication + +These are the utility components related to / used in the authentication module. + + + +::: mcproto.auth.msa.MSAAccount + options: + show_root_heading: true + show_root_toc_entry: true + filters: + - "^_[^_]" diff --git a/docs/reference/private/deprecation.md b/docs/reference/private/deprecation.md new file mode 100644 index 00000000..6eaf0e9f --- /dev/null +++ b/docs/reference/private/deprecation.md @@ -0,0 +1,3 @@ +# Deprecation utilities + +::: mcproto.utils.deprecation diff --git a/docs/reference/private/index.md b/docs/reference/private/index.md new file mode 100644 index 00000000..3d3bcdc2 --- /dev/null +++ b/docs/reference/private/index.md @@ -0,0 +1,11 @@ +# Private API Reference + +This is the reference documentation page for **private** mcproto API. + +!!! warning + + Private here means that the functions/classes documented here are only meant to be used internally by the library, + **you shouldn't use these in your code** if you're just a user of this library. This page is here mainly as a + reference for contributors and for providing proper linkable references. The backwards compatibility of these + components will not be guarranteed, which means breaking changes may be introduced between patch versinos without + any warnings. diff --git a/docs/reference/private/protocol.md b/docs/reference/private/protocol.md new file mode 100644 index 00000000..ff71b97c --- /dev/null +++ b/docs/reference/private/protocol.md @@ -0,0 +1,5 @@ +# Internal components for protocol + +These are the utility components related to / used in the protocol module. + +::: mcproto.protocol.utils diff --git a/docs/reference/private/types.md b/docs/reference/private/types.md new file mode 100644 index 00000000..b87d12df --- /dev/null +++ b/docs/reference/private/types.md @@ -0,0 +1,9 @@ +# Internal components for types + +These are the utility components related to / used in the types module. + +::: mcproto.types.nbt + options: + show_submodules: true + filters: + - "^_[^_]" diff --git a/docs/reference/public/authentication.md b/docs/reference/public/authentication.md new file mode 100644 index 00000000..f253afa0 --- /dev/null +++ b/docs/reference/public/authentication.md @@ -0,0 +1,11 @@ +# Authentication + +::: mcproto.auth.account + +::: mcproto.auth.yggdrasil + +::: mcproto.auth.msa + +::: mcproto.auth.microsoft.oauth + +::: mcproto.auth.microsoft.xbox diff --git a/docs/reference/public/encryption.md b/docs/reference/public/encryption.md new file mode 100644 index 00000000..3cf23c3e --- /dev/null +++ b/docs/reference/public/encryption.md @@ -0,0 +1,6 @@ +# Encryption utilities + +The following components are used for encryption related interacions (generally needed during the communication with +the server, after an encryption request during the login process) + +::: mcproto.encryption diff --git a/docs/reference/public/multiplayer.md b/docs/reference/public/multiplayer.md new file mode 100644 index 00000000..600d6c28 --- /dev/null +++ b/docs/reference/public/multiplayer.md @@ -0,0 +1,5 @@ +# Multiplayer utilities + +The following components are used for various multiplayer interacions (generally needed during the server joining process). + +::: mcproto.multiplayer diff --git a/docs/reference/public/packets.md b/docs/reference/public/packets.md new file mode 100644 index 00000000..39b29e24 --- /dev/null +++ b/docs/reference/public/packets.md @@ -0,0 +1,35 @@ +# Packets + +## Base classes and interaction functions + +::: mcproto.packets + options: + heading_level: 3 + +## Handshaking gamestate + +::: mcproto.packets.handshaking.handshake + options: + heading_level: 3 + +## Status gamestate + +::: mcproto.packets.status.ping + options: + heading_level: 3 + +::: mcproto.packets.status.status + options: + heading_level: 3 + +## Login gamestate + +::: mcproto.packets.login.login + options: + heading_level: 3 + +## Play gamestate + +!!! bug "Work In Progress" + + Packets for the Play gamestate aren't yet implemented. diff --git a/docs/reference/public/protocol.md b/docs/reference/public/protocol.md new file mode 100644 index 00000000..f9bfe024 --- /dev/null +++ b/docs/reference/public/protocol.md @@ -0,0 +1,9 @@ +# Protocol documentation + +This is the documentation for components related to interactions with the minecraft protocol and connection establishing. + +::: mcproto.protocol.base_io + +::: mcproto.buffer.Buffer + +::: mcproto.connection diff --git a/docs/reference/public/types.md b/docs/reference/public/types.md new file mode 100644 index 00000000..e13c6446 --- /dev/null +++ b/docs/reference/public/types.md @@ -0,0 +1,5 @@ +# Types + +::: mcproto.types + options: + show_submodules: true diff --git a/mkdocs.yml b/mkdocs.yml index faf855a6..5153644d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,6 +36,20 @@ nav: - Great commits: contributing/guides/great-commits.md - Unit Tests: contributing/guides/unit-tests.md - Deprecations: contributing/guides/deprecations.md + - API Reference: + - Protocol: reference/public/protocol.md + - Authentication: reference/public/authentication.md + - Encryption: reference/public/encryption.md + - Multiplayer: reference/public/multiplayer.md + - Types: reference/public/types.md + - Packets: reference/public/packets.md + - Private API Reference: + - reference/private/index.md + - ABCs: reference/private/abc.md + - Deprecation: reference/private/deprecation.md + - Authentication: reference/private/authentication.md + - Protocol: reference/private/protocol.md + - Types: reference/private/types.md theme: name: material @@ -105,6 +119,34 @@ plugins: - mike: canonical_version: "latest" version_selector: true + - mkdocstrings: + enable_inventory: true + default_handler: python + handlers: + python: + options: + docstring_options: + ignore_init_summary: true + show_root_heading: false + show_root_toc_entry: false + show_source: false + docstring_style: sphinx + relative_crossrefs: true + scoped_crossrefs: true + show_signature_annotations: true + signature_crossrefs: true + separate_signature: true + show_symbol_type_heading: true + show_symbol_type_toc: true + parameter_headings: true + show_object_full_path: true + docstring_section_style: table + import: + - url: https://docs.python.org/3.13/objects.inv + domains: [std, py] + - https://typing-extensions.readthedocs.io/en/latest/objects.inv + - https://cryptography.io/en/stable/objects.inv + - https://python-semanticversion.readthedocs.io/en/stable/objects.inv extra: version: diff --git a/poetry.lock b/poetry.lock index 9771bc7e..d23d1971 100644 --- a/poetry.lock +++ b/poetry.lock @@ -499,6 +499,20 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "griffe" +version = "1.5.6" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.9" +files = [ + {file = "griffe-1.5.6-py3-none-any.whl", hash = "sha256:b2a3afe497c6c1f952e54a23095ecc09435016293e77af8478ed65df1022a394"}, + {file = "griffe-1.5.6.tar.gz", hash = "sha256:181f6666d5aceb6cd6e2da5a2b646cfb431e47a0da1fda283845734b67e10944"}, +] + +[package.dependencies] +colorama = ">=0.4" + [[package]] name = "h11" version = "0.14.0" @@ -834,6 +848,22 @@ watchdog = ">=2.0" i18n = ["babel (>=2.9.0)"] min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +[[package]] +name = "mkdocs-autorefs" +version = "1.3.0" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.9" +files = [ + {file = "mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3"}, + {file = "mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + [[package]] name = "mkdocs-get-deps" version = "0.2.0" @@ -891,6 +921,50 @@ files = [ {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, ] +[[package]] +name = "mkdocstrings" +version = "0.28.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.9" +files = [ + {file = "mkdocstrings-0.28.0-py3-none-any.whl", hash = "sha256:84cf3dc910614781fe0fee46ce8006fde7df6cc7cca2e3f799895fb8a9170b39"}, + {file = "mkdocstrings-0.28.0.tar.gz", hash = "sha256:df20afef1eafe36ba466ae20732509ecb74237653a585f5061937e54b553b4e0"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.11.1" +Markdown = ">=3.6" +MarkupSafe = ">=1.1" +mkdocs = ">=1.4" +mkdocs-autorefs = ">=1.3" +mkdocs-get-deps = ">=0.2" +pymdown-extensions = ">=6.3" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.14.5" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.9" +files = [ + {file = "mkdocstrings_python-1.14.5-py3-none-any.whl", hash = "sha256:ac394f273ae298aeaa6be4506768f05e61bd7c8119437ea98553354b1185c469"}, + {file = "mkdocstrings_python-1.14.5.tar.gz", hash = "sha256:8582eeac8cce952f395d76ec636fc814757cba7d8458aa75ba0529a3aa10d98c"}, +] + +[package.dependencies] +griffe = ">=0.49" +mkdocs-autorefs = ">=1.2" +mkdocstrings = ">=0.28" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + [[package]] name = "mslex" version = "1.2.0" @@ -1715,4 +1789,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "6921a1cb55aace4b6cbaeaada7508db952769b10e5cfe6f6e045ef63c5ae590e" +content-hash = "d58080b11e4e08a15244e102d722aa30a8a72292c03fff8e09da64b3a9436642" diff --git a/pyproject.toml b/pyproject.toml index 6f6f5b88..31f1ec3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,8 @@ mkdocs = "^1.6.0" mkdocs-material = "^9.5.30" mike = "^2.1.2" markdown-exec = { extras = ["ansi"], version = "^1.9.3" } -towncrier = ">=23,<24.7" # temporary pin, as 24.7 is incompatible with sphinxcontrib-towncrier +towncrier = ">=23,<24.7" # temporary pin, as 24.7 is incompatible with sphinxcontrib-towncrier +mkdocstrings-python = { version = "^1.12.1", python = ">3.9,<4" } [tool.poetry.group.docs-ci] optional = true From bc4cc918502f9b5c1a2fbf2322dae45285cf4227 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 18 Oct 2024 06:42:32 +0200 Subject: [PATCH 53/85] Add authentication usage guide --- docs/usage/authentication.md | 258 +++++++++++++++++++++++++++++++++++ docs/usage/index.md | 15 ++ mkdocs.yml | 3 + 3 files changed, 276 insertions(+) create mode 100644 docs/usage/authentication.md create mode 100644 docs/usage/index.md diff --git a/docs/usage/authentication.md b/docs/usage/authentication.md new file mode 100644 index 00000000..55d91c17 --- /dev/null +++ b/docs/usage/authentication.md @@ -0,0 +1,258 @@ +# Minecraft account authentication + +Mcproto has first party support to handle authentication, allowing you to use your own minecraft account. This is +needed if you wish to login to "online mode" (non-warez) servers as a client (player). + +## Microsoft (migrated) accounts + +This is how authentication works for already migrated minecraft accounts, using Microsoft accounts for authentication. +(This will be most accounts. Any newly created minecraft accounts - after 2021 will always be Microsoft linked +accounts.) + +### Creating Azure application + +To authenticate with a microsoft account, you will need to go through the entire OAuth2 flow. Mcproto has functions to +hide pretty much all of this away, however you will need to create a new Microsoft Azure application, that mcproto +will use to obtain an access token. + +We know this is annoying, but it's a necessary step, as Microsoft only allows these applications to request OAuth2 +authentication, and to avoid potential abuse, we can't really just use our registered application (like with say +[MultiMC](https://github.com/MultiMC/Launcher)), as this token would have to be embedded into our source-code, and +since this is python, that would mean just including it here in plain text, and because mcproto is a low level library +that can be used for any kind of interactions, we can't trust that you won't abuse this token. + +Instead, everyone using mcproto should register a new application, and get their own MSA token for your application +that uses mcproto in the back. + +To create a new application, follow these steps (this is a simplified guide, for a full guide, feel free to check the +[Microsoft documentation](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)): + +1. Go to the [Azure portal](https://portal.azure.com/#home) and log in (create an account if you need to). +2. Search for and select **Azure Active Directory**. +3. On the left navbar, under **Manage** section, click on **App registrations**. +4. Click on **New registration** on top navbar. +5. Pick a name for the application. Anyone using your app to authenticate will see this name. +6. Choose **Personal Microsoft accounts only** from the Supported account types. +7. Leave the **Redirect URI (optional)** empty. +8. Click on **Register**. + +From there, you will need to enable this application to be used for OAuth2 flows. To do that, follow these steps: + +1. On the left navbar, under **Manage** section, click on **Authentication**. +2. Set **Allow public content flows** to **Yes**. +3. Click **Save**. + +After that, you can go back to the app (click **Overview** from the left navbar), and you'll want to copy the +**Application (client) ID**. This is the ID you will need to pass to mcproto. (You will also need the **Display name**, +and the **Directory (Tenant) ID** for [Registering the application with Minecraft] - first time only) + +If you ever need to access this application again, follow these steps (as Microsoft Azure is pretty unintuitive, we +document this too): + +1. Go to the [Azure portal](https://portal.azure.com/#home) and log in. +2. Click on **Azure Active Directory** (if you can't find it on the main page, you can use the search). +3. On the left navbar, under **Manage** section, click on **App registrations**. +4. Click on **View all applications from personal account** (assuming you registered the app from a personal account). +5. Click on your app. + +### Registering the application with Minecraft + +Previously, this step wasn't required, however due to people maliciously creating these applications to steal +accounts, Mojang have recently started to limit access to the , and only allow +explicitly white listed Client IDs to use this API. + +This API is absolutely crucial step in getting the final minecraft token, and so you will need to register your Client +ID to be white listed by Mojang. Thankfully, it looks like Mojang is generally pretty lenient and at least for me, +they didn't cause any unnecessary hassles when I asked for my application to be registered, for development purposes +and work on mcproto. + +That said, you will need to wait a while (about a week, though it could be more), until Mojang reviews your +application and approves it. There isn't much we can do about this. + +To get your Azure application registered, you will need to fill out a simple form, where you accept the EULA, provide +your E-Mail, Application name, Application Client ID and Tennant ID. + +More annoyingly you will additionally also need to provide an **associated website or domain** for your project/brand. +(This application is generally designed for more user-facing programs, such as full launchers. When registering +mcproto, I just used the GitHub URL). Lastly, you'll want to describe why you need access to this API in the +**Justification** section. + +Visit the [Mojang article](https://help.minecraft.net/hc/en-us/articles/16254801392141) describing this process. There +is also a link to the form to fill out. + +### The code + +Finally, after you've managed to register your application and get it approved by Mojang, you can use it with mcproto, +go through the Microsoft OAuth2 flow and authorize this application to access your Microsoft account, which mcproto +will then use to get the minecraft token you'll then need to login to online servers. + +```python +import httpx +from mcproto.auth.microsoft.oauth import full_microsoft_oauth +from mcproto.auth.microsoft.xbox import xbox_auth +from mcproto.auth.msa import MSAAccount + +MY_MSA_CLIENT_ID = "[REDACTED]" # Paste your own Client ID here + +async def authenticate() -> MSAAccount: + async with httpx.AsyncClient() as client: + microsoft_token = await full_microsoft_oauth(client, MY_MSA_CLIENT_ID) + user_hash, xsts_token = xbox_auth(client, microsoft_token) + return MSAAccount.xbox_auth(cilent, user_hash, xsts_token) +``` + +Note that the `full_microsoft_oauth` function will print a message containing the URL you should visit in your +browser, and a one time code to type in once you reach this URL. That will then prompt you to log in to your Microsoft +account, and then allow you to authorize the application to use your account. + +### Caching + +You will very likely want to set up caching here, and store at least the `microsoft_token` somewhere, so you don't have +to log in each time your code will run. Here's some example code that caches every step of the way, always resorting to +the "closest" functional token. Note that this is using `pickle` to store the tokens, you may want to use JSON or other +format instead, as it would be safer. Also, be aware that these are sensitive and if compromised, someone could gain +access to your minecraft account (though only for playing, they shouldn't be able to change your password or anything +like that), so you might want to consider encrypting these cache files before storing: + +```python +from __future__ import annotations + +import logging +import pickle +from pathlib import Path + +import httpx + +from mcproto.auth.microsoft.oauth import full_microsoft_oauth +from mcproto.auth.microsoft.xbox import XSTSRequestError, xbox_auth +from mcproto.auth.msa import MSAAccount, ServicesAPIError + +log = logging.getLogger(__name__) + +MY_MSA_CLIENT_ID = "[REDACTED]" # Paste your own Client ID here +CACHE_DIR = Path(".cache/") + + +async def microsoft_login(client: httpx.AsyncClient) -> MSAAccount: # noqa: PLR0912,PLR0915 + """Obtain minecraft account using Microsoft authentication. + + This function performs full caching of every step along the way, allowing for recovery + without manual intervention for as long as at least the root token (from Microsoft OAuth2) + is valid. Any later tokens will be refreshed and re-cached once invalid. + + If all tokens are invalid, or this function was ran for the first time (without any cached + data), you will be shown a URL and a code. You have to go to this URL with your browser and + enter the code, completing the OAuth2 flow, obtaining the root token. + """ + CACHE_DIR.mkdir(parents=True, exist_ok=True) + + access_token_cache = CACHE_DIR.joinpath("xbox_access_token.pickle") + if access_token_cache.exists(): + with access_token_cache.open("rb") as f: + access_token: str = pickle.load(f) # noqa: S301 + + try: + account = await MSAAccount.from_xbox_access_token(client, access_token) + log.info("Logged in with cached xbox minecraft access token") + return account + except httpx.HTTPStatusError as exc: + log.warning(f"Cached xbox minecraft access token is invalid: {exc!r}") + else: + log.warning("No cached access token available, trying Xbox Secure Token Service (XSTS) token") + + # Access token either doesn't exist, or isn't valid, try XSTS (Xbox) token + xbox_token_cache = CACHE_DIR.joinpath("xbox_xsts_token.pickle") + if xbox_token_cache.exists(): + with xbox_token_cache.open("rb") as f: + user_hash, xsts_token = pickle.load(f) # noqa: S301 + + try: + access_token = await MSAAccount._get_access_token_from_xbox(client, user_hash, xsts_token) + except ServicesAPIError as exc: + log.warning(f"Invalid cached Xbox Secure Token Service (XSTS) token: {exc!r}") + else: + log.info("Obtained xbox access token from cached Xbox Secure Token Service (XSTS) token") + log.info("Storing xbox minecraft access token to cache and restarting auth") + with access_token_cache.open("wb") as f: + pickle.dump(access_token, f) + return await microsoft_login(client) + else: + log.warning("No cached Xbox Secure Token Service (XSTS) token available, trying Microsoft OAuth2 token") + + # XSTS token either doesn't exist, or isn't valid, try Microsoft OAuth2 token + microsoft_token_cache = CACHE_DIR.joinpath("microsoft_token.pickle") + if microsoft_token_cache.exists(): + with microsoft_token_cache.open("rb") as f: + microsoft_token = pickle.load(f) # noqa: S301 + + try: + user_hash, xsts_token = await xbox_auth(client, microsoft_token) + except (httpx.HTTPStatusError, XSTSRequestError) as exc: + log.warning(f"Invalid cached Microsoft OAuth2 token {exc!r}") + else: + log.info("Obtained Xbox Secure Token Service (XSTS) token from cached Microsoft OAuth2 token") + log.info("Storing Xbox Secure Token Service (XSTS) token to cache and restarting auth") + with xbox_token_cache.open("wb") as f: + pickle.dump((user_hash, xsts_token), f) + return await microsoft_login(client) + else: + log.warning("No cached microsoft token") + + # Microsoft OAuth2 token either doesn't exist, or isn't valid, request user auth + log.info("Running Microsoft OAuth2 flow, requesting user authentication") + microsoft_token = await full_microsoft_oauth(client, MY_MSA_CLIENT_ID) + log.info("Obtained Microsoft OAuth2 token from user authentication") + log.info("Storing Microsoft OAuth2 token and restarting auth") + with microsoft_token_cache.open("wb") as f: + pickle.dump(microsoft_token["access_token"], f) + return await microsoft_login(client) +``` + +## Minecraft (non-migrated) accounts + +If you haven't migrated your account into a Microsoft account, follow this guide for authentication. (Any newly created +Minecraft accounts will be using Microsoft accounts already.) This method of authentication is called "yggdrasil". + +!!! warning + + The account migration process has been concluded in **September 19, 2023**. See: + + + That means that it's no longer possible to migrate this old account into a microsoft account and it's only a matter + of time until the authentication servers handling these accounts are turned off entirely. + + Mcproto will remove support for this old authentication methods once this happens. + +This method of authentication doesn't require any special app registrations, however it is significantly less secure, +as you need to enter your login and password directly. + +```python + import httpx + from mcproto.auth.yggdrasil import YggdrasilAccount + + LOGIN = "mail@example.com" + PASSWORD = "my_password" + + async def authenticate() -> YggdrasilAccount: + async with httpx.AsyncClient() as client: + return YggdrasilAccount.authenticate(client, login=LOGIN, password=PASSWORD) +``` + +The Account instance you will obtain here will contain a refresh token, and a shorter lived access token, received from +Mojang APIs from the credentials you entered. Just like with Microsoft accounts, you may want to cache these tokens to +avoid needless calls to request new ones and go through authentication again. That said, since doing so doesn't +necessarily require user interaction, if you make the credentials accessible from your code directly, this is a lot +less annoying. + +If you will decide to use caching, or if you plan on using these credentials in a long running program, you may see the +access token expire. You can check whether the token is expired with the `YggdrasilAccount.validate` method, and if it +is (call returned `False`), you can call `YggdrasilAccount.refresh` to use the refresh token to obtain a new access +token. The refresh token is much more long lived than the access token, so this should generally be enough for you, +although if you login from elsewhere, or after a really long time, the refresh token might be invalidated, in that +case, you'll need to go through the full login again. + +## Legacy Mojang accounts + +If your minecraft account is still using the (really old) Mojang authentication, you can simply follow the non-migrated +guide, as it will work with these legacy accounts too, the only change you will need to make is to use your username, +instead of an email. diff --git a/docs/usage/index.md b/docs/usage/index.md new file mode 100644 index 00000000..b04591fa --- /dev/null +++ b/docs/usage/index.md @@ -0,0 +1,15 @@ +# Usage + +This part of the documentation contains various guides and explanations on how to use the different parts of mcproto. + +!!! bug "Work In Progress" + + This category is still being written. Many pages are missing. + +!!! note "Didn't find what you were looking for?" + + If you were looking for a guide on something, but you didn't find it documented here and you feel like it's + something that others would benefit from seeing too, you can create a [github + issue](../contributing/reporting-a-bug.md) and ask us to write one. + + diff --git a/mkdocs.yml b/mkdocs.yml index 5153644d..b478f9e5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,9 @@ nav: - Installation: installation/index.md - Version Guarantees: installation/version-guarantees.md - Changelog: installation/changelog.md + - Usage: + - usage/index.md + - Authentication: usage/authentication.md - Community: - Code of Conduct: community/code-of-conduct.md - Attributions: community/attribution.md From fb0cc8a0a3e7e84cbd382eda2f5545b21193a45d Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 20 Oct 2024 19:08:26 +0200 Subject: [PATCH 54/85] Improve wording in coc --- docs/community/code-of-conduct.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/community/code-of-conduct.md b/docs/community/code-of-conduct.md index 1550b780..c0f34cd0 100644 --- a/docs/community/code-of-conduct.md +++ b/docs/community/code-of-conduct.md @@ -19,17 +19,17 @@ correct answer. ## Harassment We share a common understanding of what constitutes harassment as it applies to a professional setting. Although this -list cannot be exhaustive, we explicitly honor diversity in age, gender, culture, ethnicity, language, national origin, -political beliefs, profession, race, religion, sexual orientation, socioeconomic status, disability and personal -appearance. We will not tolerate discrimination based on any of the protected characteristics above, including some -that may not have been explicitly mentioned here. We consider discrimination of any kind to be unacceptable and -immoral. +list cannot be exhaustive, we explicitly honor the following "protected attributes": **diversity in age, gender, +culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, +socioeconomic status, disability and personal appearance**. We will not tolerate discrimination based on any of the +protected characteristics above, including some that may not have been explicitly mentioned here. We consider +discrimination of any kind to be unacceptable and immoral. Harassment includes, but is not limited to: - Offensive comments (or "jokes") related to any of the above mentioned attributes. - Deliberate "outing"/"doxing" of any aspect of a person's identity, such as physical or electronic address, without - their explicit consent, except as necessary to protect others from intentional abuse. + their explicit consent, except as necessary means to protect others from intentional abuse. - Unwelcome comments regarding a person's lifestyle choices and practices, including those related to food, health, parenting, drugs and employment. - Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a From f0cdfb4cf89ac11f8a85a2165ff302e751706267 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 22 Oct 2024 12:11:08 +0200 Subject: [PATCH 55/85] Properly show all private members --- docs/reference/private/authentication.md | 8 ++------ docs/reference/private/protocol.md | 6 ++++++ docs/reference/private/types.md | 23 +++++++++++++++++++++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/docs/reference/private/authentication.md b/docs/reference/private/authentication.md index bd1625d9..38f6224f 100644 --- a/docs/reference/private/authentication.md +++ b/docs/reference/private/authentication.md @@ -2,12 +2,8 @@ These are the utility components related to / used in the authentication module. - - -::: mcproto.auth.msa.MSAAccount +::: mcproto.auth.msa options: - show_root_heading: true - show_root_toc_entry: true + members: [MSAAccount] filters: - "^_[^_]" diff --git a/docs/reference/private/protocol.md b/docs/reference/private/protocol.md index ff71b97c..bfe6d87c 100644 --- a/docs/reference/private/protocol.md +++ b/docs/reference/private/protocol.md @@ -3,3 +3,9 @@ These are the utility components related to / used in the protocol module. ::: mcproto.protocol.utils + +::: mcproto.protocol.base_io + options: + members: [BaseAsyncWriter, BaseSyncWriter, BaseAsyncReader, BaseSyncReader] + filters: + - "^_[^_]" diff --git a/docs/reference/private/types.md b/docs/reference/private/types.md index b87d12df..866f269d 100644 --- a/docs/reference/private/types.md +++ b/docs/reference/private/types.md @@ -2,8 +2,27 @@ These are the utility components related to / used in the types module. -::: mcproto.types.nbt +::: mcproto.types.nbt.NBTag options: - show_submodules: true + show_root_heading: true + show_root_toc_entry: true filters: - "^_[^_]" + +::: mcproto.types.nbt._NumberNBTag + options: + show_root_heading: true + show_root_toc_entry: true + members: [payload] + +::: mcproto.types.nbt._FloatingNBTag + options: + show_root_heading: true + show_root_toc_entry: true + members: [payload] + +::: mcproto.types.nbt._NumberArrayNBTag + options: + show_root_heading: true + show_root_toc_entry: true + members: [payload] From 86f824b5ce68803346d16a6ec80a00b024baffc5 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 22 Oct 2024 12:29:28 +0200 Subject: [PATCH 56/85] Add private api docs for packets --- docs/reference/private/packets.md | 18 ++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 19 insertions(+) create mode 100644 docs/reference/private/packets.md diff --git a/docs/reference/private/packets.md b/docs/reference/private/packets.md new file mode 100644 index 00000000..bc8b4fdc --- /dev/null +++ b/docs/reference/private/packets.md @@ -0,0 +1,18 @@ +# Internal components for packets + +These are the utility components related to / used in the packets module. + +!!! bug "Missing internal components of individual packets" + + The internal components which are specific to the individual packet classes (rather than being shared across the + entire packets module) are not documented here. Documentation for these may be added in the future, but there is + no timeframe for this, if you're interested in these, we recommend that you just check the source code instead. + +::: mcproto.packets.packet_map + options: + members: [WalkableModuleData, _walk_submodules, _walk_module_packets] + +::: mcproto.packets.interactions + options: + filters: + - "^_[^_]" diff --git a/mkdocs.yml b/mkdocs.yml index b478f9e5..6512a3f2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,6 +53,7 @@ nav: - Authentication: reference/private/authentication.md - Protocol: reference/private/protocol.md - Types: reference/private/types.md + - Packets: reference/private/packets.md theme: name: material From d25424ca82005cf3f39e65be5f7b51cbf2bd7453 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 22 Oct 2024 12:35:51 +0200 Subject: [PATCH 57/85] Add notice for pending rewrite of packets docs --- docs/reference/public/packets.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/public/packets.md b/docs/reference/public/packets.md index 39b29e24..08f72ab9 100644 --- a/docs/reference/public/packets.md +++ b/docs/reference/public/packets.md @@ -1,5 +1,11 @@ # Packets +!!! bug "Pending rewrite of this page" + + This page will be rewritten in the near future and split it into multiple pages for the individual game states, + with the play state possibly being subdivided into even more pages. Currently, this page shows all implemented + packets in mcproto. This split will happen once play state packets are introduced. + ## Base classes and interaction functions ::: mcproto.packets From 1e1356f87e6c5b3d48e5e85176620c6194d7c6e9 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 22 Oct 2024 12:36:07 +0200 Subject: [PATCH 58/85] Show the buffer class properly --- docs/reference/public/protocol.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/public/protocol.md b/docs/reference/public/protocol.md index f9bfe024..9b85fc0d 100644 --- a/docs/reference/public/protocol.md +++ b/docs/reference/public/protocol.md @@ -5,5 +5,8 @@ This is the documentation for components related to interactions with the minecr ::: mcproto.protocol.base_io ::: mcproto.buffer.Buffer + options: + show_root_heading: true + show_root_toc_entry: true ::: mcproto.connection From 70f6eba8de61822de7f0c222811e9a375195b01d Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 22 Oct 2024 12:46:39 +0200 Subject: [PATCH 59/85] Remove unnecessary indent from exaple code in docs --- docs/usage/authentication.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/usage/authentication.md b/docs/usage/authentication.md index 55d91c17..9bdd306d 100644 --- a/docs/usage/authentication.md +++ b/docs/usage/authentication.md @@ -227,15 +227,15 @@ This method of authentication doesn't require any special app registrations, how as you need to enter your login and password directly. ```python - import httpx - from mcproto.auth.yggdrasil import YggdrasilAccount +import httpx +from mcproto.auth.yggdrasil import YggdrasilAccount - LOGIN = "mail@example.com" - PASSWORD = "my_password" +LOGIN = "mail@example.com" +PASSWORD = "my_password" - async def authenticate() -> YggdrasilAccount: - async with httpx.AsyncClient() as client: - return YggdrasilAccount.authenticate(client, login=LOGIN, password=PASSWORD) +async def authenticate() -> YggdrasilAccount: + async with httpx.AsyncClient() as client: + return YggdrasilAccount.authenticate(client, login=LOGIN, password=PASSWORD) ``` The Account instance you will obtain here will contain a refresh token, and a shorter lived access token, received from From d18a1871df86c1fb311fa28c1642e260294691f4 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 22 Oct 2024 15:13:30 +0200 Subject: [PATCH 60/85] Add copyright notice to footer --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 6512a3f2..7979d26b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,6 @@ site_name: MCPROTO site_url: https://py-mine.github.io/mcproto +copyright: Mcproto Documentation © 2024 by ItsDrike repo_url: https://github.com/py-mine/mcproto repo_name: py-mine/mcproto From 9840a3f3b2eb95a5ec858ef84756927a50a0e7ce Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Tue, 22 Oct 2024 17:24:12 +0200 Subject: [PATCH 61/85] Add custom css for mkdocstrings --- LICENSE-THIRD-PARTY.txt | 20 +++++++++++++++++ docs/css/mkdocstrings.css | 47 +++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 3 +++ 3 files changed, 70 insertions(+) create mode 100644 docs/css/mkdocstrings.css diff --git a/LICENSE-THIRD-PARTY.txt b/LICENSE-THIRD-PARTY.txt index cb47e9b7..710c99db 100644 --- a/LICENSE-THIRD-PARTY.txt +++ b/LICENSE-THIRD-PARTY.txt @@ -40,6 +40,26 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------------------------------------------------- + ISC License + +Applies to: + - Copyright (c) 2021, Timothée Mazzucotelli + All rights reserved. + - docs/css/mkdocstrings.css: Entire file + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + --------------------------------------------------------------------------------------------------- GNU LESSER GENERAL PUBLIC LICENSE Applies to: diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css new file mode 100644 index 00000000..287a5dbd --- /dev/null +++ b/docs/css/mkdocstrings.css @@ -0,0 +1,47 @@ +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: 0.05rem solid var(--md-typeset-table-color); +} + +/* Mark external links with an arrow. */ +a.external::after, +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + mask-image: url('data:image/svg+xml,'); + -webkit-mask-image: url('data:image/svg+xml,'); + content: " "; + + display: inline-block; + vertical-align: middle; + position: relative; + + height: 1em; + width: 1em; + background-color: currentColor; +} + +a.external:hover::after, +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} + +pre :is(a.external, a.autorefs-external)::after { + content: none; /* Remove the arrow icon for links inside
: in the signature */
+}
+
+/* Light blue color for parameter `param` symbols`. */
+[data-md-color-scheme="default"] {
+  --doc-symbol-parameter-fg-color: #829bd1;
+  --doc-symbol-parameter-bg-color: #829bd11a;
+}
+
+[data-md-color-scheme="slate"] {
+  --doc-symbol-parameter-fg-color: #829bd1;
+  --doc-symbol-parameter-bg-color: #829bd11a;
+}
+
+/* Hide parameter 'param' symbols in ToC. */
+li.md-nav__item:has(> a[href*="("]) {
+  display: none;
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 7979d26b..7166ad5a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -99,6 +99,9 @@ theme:
     - navigation.top
     - toc.follow
 
+extra_css:
+  - css/mkdocstrings.css
+
 markdown_extensions:
   - admonition
   - attr_list

From f0070d5c33a719c5f6b80915b619c36bb2d4df54 Mon Sep 17 00:00:00 2001
From: ItsDrike 
Date: Tue, 22 Oct 2024 17:40:38 +0200
Subject: [PATCH 62/85] Add extra css styles for material theme

---
 LICENSE-THIRD-PARTY.txt | 1 +
 docs/css/material.css   | 4 ++++
 mkdocs.yml              | 1 +
 3 files changed, 6 insertions(+)
 create mode 100644 docs/css/material.css

diff --git a/LICENSE-THIRD-PARTY.txt b/LICENSE-THIRD-PARTY.txt
index 710c99db..3e9704f6 100644
--- a/LICENSE-THIRD-PARTY.txt
+++ b/LICENSE-THIRD-PARTY.txt
@@ -47,6 +47,7 @@ Applies to:
     - Copyright (c) 2021, Timothée Mazzucotelli
       All rights reserved.
         - docs/css/mkdocstrings.css: Entire file
+        - docs/css/material.css: Entire file
 
 Permission to use, copy, modify, and/or distribute this software for any
 purpose with or without fee is hereby granted, provided that the above
diff --git a/docs/css/material.css b/docs/css/material.css
new file mode 100644
index 00000000..51ae054a
--- /dev/null
+++ b/docs/css/material.css
@@ -0,0 +1,4 @@
+/* Don't uppercase H5 headings. */
+.md-typeset h5 {
+  text-transform: none;
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 7166ad5a..9c429984 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -101,6 +101,7 @@ theme:
 
 extra_css:
   - css/mkdocstrings.css
+  - css/material.css
 
 markdown_extensions:
   - admonition

From 04a06d6fc205b8707c2794659878ade6fac65a9d Mon Sep 17 00:00:00 2001
From: ItsDrike 
Date: Thu, 24 Oct 2024 18:49:48 +0200
Subject: [PATCH 63/85] Add custom important admodition

---
 docs/css/admoditions.css | 18 ++++++++++++++++++
 mkdocs.yml               |  1 +
 2 files changed, 19 insertions(+)
 create mode 100644 docs/css/admoditions.css

diff --git a/docs/css/admoditions.css b/docs/css/admoditions.css
new file mode 100644
index 00000000..f4132550
--- /dev/null
+++ b/docs/css/admoditions.css
@@ -0,0 +1,18 @@
+/* Add important admonition */
+:root {
+  --md-admonition-icon--important: url("data:image/svg+xml,");
+}
+.md-typeset .admonition.important,
+.md-typeset details.important {
+  border-color: rgb(171, 125, 248);
+}
+.md-typeset .important > .important,
+.md-typeset .important > summary {
+  background-color: rgba(171, 125, 248, 0.1);
+}
+.md-typeset .important > .admonition-title::before,
+.md-typeset .important > summary::before {
+  background-color: rgb(171, 125, 248);
+  -webkit-mask-image: var(--md-admonition-icon--important);
+  mask-image: var(--md-admonition-icon--important);
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 9c429984..22a82c7b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -102,6 +102,7 @@ theme:
 extra_css:
   - css/mkdocstrings.css
   - css/material.css
+  - css/admoditions.css
 
 markdown_extensions:
   - admonition

From 1c923fd29fa5effc6839aa5e40d61a30d0a97762 Mon Sep 17 00:00:00 2001
From: ItsDrike 
Date: Thu, 24 Oct 2024 20:36:06 +0200
Subject: [PATCH 64/85] Write breaking changes & deprecations guide

---
 docs/contributing/guides/breaking-changes.md | 249 +++++++++++++++++++
 docs/contributing/guides/deprecations.md     |   3 -
 mkdocs.yml                                   |   2 +-
 3 files changed, 250 insertions(+), 4 deletions(-)
 create mode 100644 docs/contributing/guides/breaking-changes.md
 delete mode 100644 docs/contributing/guides/deprecations.md

diff --git a/docs/contributing/guides/breaking-changes.md b/docs/contributing/guides/breaking-changes.md
new file mode 100644
index 00000000..0da91611
--- /dev/null
+++ b/docs/contributing/guides/breaking-changes.md
@@ -0,0 +1,249 @@
+# Breaking changes & Deprecations
+
+???+ abstract
+
+    This page describes how we handle breaking changes and deprecations in the project. It clarifies what is a breaking
+    change, what is a deprecation, how to mark something as deprecated and explains when a function should be
+    deprecated. Finally, it mentions how to properly communicate breaking chagnges and deprecations to end users.
+
+## What is a breaking change
+
+A breaking change is a change in the code that causes previously working functionality to break in a way that requires
+developers to modify their existing code. We have a list of examples detailing what we consider as a breaking change in
+the [version guarantees page](../../installation/version-guarantees.md#examples-of-breaking-changes), and it is crucial
+that you familiarize yourself with it.
+
+!!! important
+
+    Breaking changes should be **avoided** whenever possible, as they disrupt users by forcing immediate updates to their
+    codebase.
+
+    When introducing changes, aim to implement them in a non-breaking way. If a breaking change is absolutely
+    necessary, strive to transition gradually through deprecations.
+
+## What is a deprecation
+
+A deprecation signals that a particular part of the code (commonly a function, class, or argument) should no longer be
+used because it is outdated, inefficient, or replaced by better alternatives. Deprecations are a **temporary** measure
+to guide developers toward newer practices, while giving them time to adjust their code without causing immediate
+disruptions.
+
+Deprecations act as a soft warning: they indicate that the deprecated feature will eventually be removed, but for now,
+it remains usable with a runtime deprecation warning. This gives developers enough time to adapt before the removal
+takes place in a future major release.
+
+It’s essential to understand that deprecations are not permanent — every deprecated feature has a defined removal
+version, beyond which it will no longer exist. Typically, the removal happens in the next major version after the
+deprecation was announced. For example, if a feature is deprecated in version 3.x, it will usually be removed in
+version 4.0.0.
+
+Deprecations are primarily used for:
+
+- **Phasing out old functions, classes, or methods** in favor of improved alternatives.
+- **Renaming functions, arguments, or classes** to align with better conventions.
+- **Adjusting method signatures**, such as adding required arguments or removing old ones.
+- **Changing behaviors** that can’t be applied retroactively without introducing errors.
+
+!!! note
+
+    Sometimes, it isn't possible/feasible to deprecate something, as the new change is so different from the original
+    that a breaking change is the only option. That said, this should be a rare case and you should always first do
+    your best to think about how to deprecate something before deciding on just marking your change as breaking.
+
+## How to deprecate
+
+We have two custom function to mark something as deprecated, both of these live in the `mcproto.utils.deprecation`
+module:
+
+- `deprecation_warn`: This function triggers a deprecation warning immediately after it is called, alerting developers
+    to the pending removal.
+- `deprecated`: This decorator function marks a function as deprecated. It triggers a deprecation warning each time the
+    decorated function is called. Internally, this ultimately just calls `deprecation_warn`.
+
+### Removal version
+
+These functions take a removal version as an argument, which should be specified as a [semantic
+version](https://semver.org/) string. Generally, you'll just want to put the next major version of the library here (so
+if you're currently on `3.5.2` you'll want to specify the removal version as `4.0.0`; You always want to bump the first
+/ major version number.)
+
+The `deprecation_warn` function will usually just show a warning, however, if the current version of the library
+surpasses the removal version, it will instead throw a runtime error, making it unusable. In most cases, people
+shouldn't ever face this, as once the new major version is released, all deprecations with that removal version should
+be removed, but it's a nice way to ensure the proper behavior, just in case we'd forget, allowing us to remove them
+later on in a patch version without breaking the semantic versioning model.
+
+!!! note
+
+    The removal version is a **required** argument, as we want to make sure that deprecated code doesn't stay in our
+    codebase forever. Deprecations should always be a temporary step toward the eventual removal of a feature.
+
+    If there is a valid reason to extend the deprecation period, you can push back the removal version, keeping the old
+    or compatibility code longer and incrementing the major version number in the argument accordingly.
+
+    However, we should **never** shorten the deprecation period, as that would defeat the purpose of giving developers
+    enough time to adapt to the change. Reducing the deprecation time could result in unexpected breakage for users
+    relying on the deprecated feature.
+
+### Examples
+
+#### Function rename
+
+```python
+from mcproto.utils.deprecation import deprecated
+
+
+@deprecated(removal_version="4.0.0", replacement="new_function")
+def old_function(x: int, y: int) -> int:
+  ...
+
+
+def new_function(x: int, y: int) -> int:
+  ...
+```
+
+#### Class removal
+
+```python
+@deprecated(removal_version="4.0.0", extra_msg="Optional extra message")
+class MyClass:
+  ...
+```
+
+#### Argument removal
+
+```python
+from mcproto.utils.deprecation import deprecation_warn
+
+def old_function(x: int, y: int, z: int) -> int:
+  ...
+
+def new_function(x: int, y: int, z: int | None = None) -> int:
+  if z is not None:
+    deprecation_warn(
+      obj_name="z (new_function argument)",
+      removal_version="4.0.0",
+      replacement=None,
+      extra_msg="Optional extra message, like a reason for removal"
+    )
+
+  ...  # this logic should still support working with z, just like it did in the old impl
+```
+
+## Communicating breaking changes
+
+**Breaking changes necessitate clear communication**, as they directly impact users by forcing updates to their
+codebases. It’s essential to ensure that users are well-informed about any breaking changes introduced in the project.
+This is achieved through the project’s changelog.
+
+**Every breaking change must be documented using a 'breaking' type changelog fragment.** When writing the fragment,
+adhere to the following guidelines:
+
+- Specify **what** was deprecated with a fully qualified name (e.g. `module.submodule.MyClass.deprecated_method`).
+- Suggest an **alternative**, if applicable, and explain any necessary **migration steps**.
+- Briefly document **why** the deprecation was made (without going into excessive detail).
+- Prioritize **clarity and good wording**
+
+These entries are critical, as they are likely to be read by end-users of our library (programmers but
+non-contributors). Keep this in mind when crafting breaking change fragments.
+
+!!! warning "Every breaking change needs its own entry"
+
+    If your pull request introduces multiple breaking changes across different components, you must create individual
+    changelog entries for each change.
+
+!!! example "Example of a good breaking changelog fragment"
+
+    Suppose a library changes the return type of a function from a list to a set. This type change would be difficult
+    to deprecate because the change affects existing code that relies on the specific return type.
+
+    ```markdown title="changes/521.breaking.md"
+    Change return type of `mcproto.utils.get_items` from `list[str]` to `set[str]`.
+
+        This change was made to improve performance and ensure unique item retrieval.
+        The previous behavior of returning duplicates in a list has been removed,
+        which may impact existing code that relies on the previous return type.
+        Users should adjust their code to handle the new return type accordingly.
+    ```
+
+    Even though it’s technically feasible to implement this as a non-breaking change - such as by creating a new
+    function or adding a boolean flag to control the return type, these approaches may not suit our use case. For
+    instance, if we were to introduce a boolean flag, we would need to set it to `False` by default and show
+    deprecation warnings to users unless they explicitly set the flag to `True`.
+
+    Eventually, when the deprecation period is over, the flag becomes pointless, but removing support for it would
+    necessitate yet another round of deprecation for the flag itself, forcing users to revert to using the function
+    without it. This approach could frustrate users and create unnecessary complexity.
+
+    When considering non-breaking changes, it’s crucial to evaluate potential complications like these. If you opt for
+    a breaking change, be sure to include similar reasoning in your pull request description to help convey the
+    rationale behind the decision.
+
+!!! note "Removing deprecations"
+
+    We consider deprecation removals as a breaking change, which means that these removals also need to be documented.
+    That said, it is sufficient for these removals to be documented in a single changelog fragment. These removals
+    alongside with writing the fragment will be performed by the project maintainers at the time of the release.
+
+## Communicating deprecations
+
+Even though a deprecation doesn’t immediately break code, it signals an upcoming change and it's essential to communicate
+this clearly to the users of our project. We achieve this through the project's changelog.
+
+???+ tip "Benefits of tracking deprecations in changelog"
+
+    While runtime deprecation warnings provide immediate feedback upon updating the library, it can often be beneficial
+    to give users a chance to plan ahead before updating the library, especially for projects that perform automatic
+    dependency updates through CI, which may not check for warnings, leading to deprecation warnings reaching
+    production.
+
+    Additionally, it's often easy for people to miss/overlook the warnings if they're not looking for them in the CLI
+    output, or if their project already produces some other warnings, making ours blend in.
+
+    By clearly documenting deprecations, we enable users to identify deprecated features before upgrading, allowing
+    them to address issues proactively or at least prepare for changes.
+
+    A changelog entry serves as a permanent, versioned record of changes, providing detailed explanations of why a
+    feature is deprecated, what the recommended replacements are. It's a place where people may look for clarification
+    on why something was removed, or in search of migration steps after seeing the deprecation warning.
+
+**Every deprecation must be documented using a 'deprecation' type changelog fragment.** When writing the fragment,
+similar guidelines to writing breaking changelog fragments apply:
+
+
+ +- Provide the **removal version** i.e. version in which the deprecated feature will be removed (e.g. `4.0.0`). (1) +- Specify **what** was deprecated with a fully qualified name (e.g. `module.submodule.MyClass.deprecated_method`). +- Suggest an **alternative**, if applicable, and explain any necessary **migration steps**. +- Briefly document **why** the deprecation was made (without going into excessive detail). +- Prioritize **clarity and good wording** + +
+ +1. This point is specific to deprecations, it's the only additional point in comparison to the breaking changes + guidelines. + +These entries form the second most important part of the changelog, likely to be read by end-users. Keep this in mind +when crafting deprecation fragments. + +!!! warning "Every deprecated component needs it's own entry" + + Just like with breaking changes, if your're deprecating multiple different components, you + must make multiple changelog entries, one for each deprecation. + +!!! example "Example of a good deprecation changelog fragment" + + Suppose we used a simple string configuration parameter but introduced a more flexible configuration object to + allow for future extensions and better validation. This would be a good candidate for deprecation rather than an + immediate breaking change. + + ```markdown title="changes/521.deprecation.md" + Deprecate string-based `mcproto.utils.connect` configuration attribute in favor of `mcproto.utils.ConnectionConfig`. + + The new `ConnectionConfig` object offers more flexibility by allowing users to specify multiple options (like + timeouts, retries, etc.) in a structured way, instead of relying on a string. Users are encouraged to migrate + to this object when calling `mcproto.utils.connect` to take full advantage of future improvements and + additional connection parameters. + + - The string-based configuration support will be removed in version `4.0.0`. + ``` diff --git a/docs/contributing/guides/deprecations.md b/docs/contributing/guides/deprecations.md deleted file mode 100644 index fb61abca..00000000 --- a/docs/contributing/guides/deprecations.md +++ /dev/null @@ -1,3 +0,0 @@ -!!! bug "Work In Progress" - - This page is still being written. The content below (if any) may change. diff --git a/mkdocs.yml b/mkdocs.yml index 22a82c7b..0e7a2713 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,7 +39,7 @@ nav: - Changelog: contributing/guides/changelog.md - Great commits: contributing/guides/great-commits.md - Unit Tests: contributing/guides/unit-tests.md - - Deprecations: contributing/guides/deprecations.md + - Breaking Changes: contributing/guides/breaking-changes.md - API Reference: - Protocol: reference/public/protocol.md - Authentication: reference/public/authentication.md From 7f416a70954c23bd6549c2445c27b3642bbc7029 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 24 Oct 2024 20:40:39 +0200 Subject: [PATCH 65/85] Add wip documentation guide --- docs/contributing/guides/documentation.md | 5 +++++ mkdocs.yml | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/contributing/guides/documentation.md diff --git a/docs/contributing/guides/documentation.md b/docs/contributing/guides/documentation.md new file mode 100644 index 00000000..92f98b9c --- /dev/null +++ b/docs/contributing/guides/documentation.md @@ -0,0 +1,5 @@ +!!! bug "Work In Progress" + + This page is still being written. The content below (if any) may change. + +# Writing documentation diff --git a/mkdocs.yml b/mkdocs.yml index 0e7a2713..bba33890 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Great commits: contributing/guides/great-commits.md - Unit Tests: contributing/guides/unit-tests.md - Breaking Changes: contributing/guides/breaking-changes.md + - Documentation: contributing/guides/documentation.md - API Reference: - Protocol: reference/public/protocol.md - Authentication: reference/public/authentication.md From 4803fe1fb62c7cf8188c51028c661a9084c50864 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 24 Oct 2024 20:50:21 +0200 Subject: [PATCH 66/85] Use important block for code quality requiements --- docs/contributing/making-a-pr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/making-a-pr.md b/docs/contributing/making-a-pr.md index a19cf1aa..43a018e0 100644 --- a/docs/contributing/making-a-pr.md +++ b/docs/contributing/making-a-pr.md @@ -11,7 +11,7 @@ To contribute, you can create a [pull request](https://docs.github.com/en/pull-r Your pull request will then be reviewed by our maintainers, and once approved, it will be merged into the main repository. Contributions can include bug fixes, documentation updates, or new features. -!!! danger "Code quality requirements" +!!! important "Code quality requirements" While we encourage and appreciate contributions, maintaining high code quality is crucial to us. That means you will need to adhere to our code quality standards. Contributions may be rejected if they do not meet these From c341155f5e5065b4f01545e313fff65bdcaa7bb5 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 24 Oct 2024 20:53:46 +0200 Subject: [PATCH 67/85] Use tip block for guide skipping paragraph --- docs/contributing/guides/index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/contributing/guides/index.md b/docs/contributing/guides/index.md index 365572d1..96711e41 100644 --- a/docs/contributing/guides/index.md +++ b/docs/contributing/guides/index.md @@ -61,9 +61,11 @@ We understand that going through all of these guidelines can be time-consuming a strongly encourage you to review them, especially if you haven't worked with these tools or followed such best practices before. -Every page in this contributing guides category has an abstract at the top, summarizing its content. This allows you to -quickly determine if you are already familiar with the topic or, if you're re-reading, to quickly recall what the page -covers. Feel free to skip any guide pages if you're already familiar with what they cover. +!!! tip + + Every page in this contributing guides category has an abstract at the top, summarizing its content. This allows + you to quickly determine if you are already familiar with the topic or, if you're re-reading, to quickly recall + what the page covers. Feel free to skip any guide pages if you're already familiar with what they cover. We believe these guides will be beneficial to you beyond our codebase, as they promote good coding practices and help make your code cleaner. You will likely be able to apply much of the knowledge you gain here to your own projects. From 700b8695dba65da8b88b4e1b7b31609d3b8fff78 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 24 Oct 2024 23:18:02 +0200 Subject: [PATCH 68/85] Explain the versioning model of mcproto --- docs/contributing/guides/breaking-changes.md | 46 +++++--- docs/index.md | 9 ++ docs/installation/version-guarantees.md | 38 ------- docs/installation/versioning-model.md | 105 +++++++++++++++++++ mkdocs.yml | 2 +- 5 files changed, 148 insertions(+), 52 deletions(-) delete mode 100644 docs/installation/version-guarantees.md create mode 100644 docs/installation/versioning-model.md diff --git a/docs/contributing/guides/breaking-changes.md b/docs/contributing/guides/breaking-changes.md index 0da91611..5fc4ef4c 100644 --- a/docs/contributing/guides/breaking-changes.md +++ b/docs/contributing/guides/breaking-changes.md @@ -6,27 +6,34 @@ change, what is a deprecation, how to mark something as deprecated and explains when a function should be deprecated. Finally, it mentions how to properly communicate breaking chagnges and deprecations to end users. +!!! note "Pre-Requisites" + + Before reading this page, make sure to familiarize yourself with our [versioning + model](../../installation/versioning-model.md) + + ## What is a breaking change -A breaking change is a change in the code that causes previously working functionality to break in a way that requires -developers to modify their existing code. We have a list of examples detailing what we consider as a breaking change in -the [version guarantees page](../../installation/version-guarantees.md#examples-of-breaking-changes), and it is crucial -that you familiarize yourself with it. +A breaking change is a modification that requires developers to adjust their code due to alterations that break +previously working functionality. This includes changes such as altering method signatures, changing return types, or +removing classes or functions without prior warning. -!!! important +We follow [semantic versioning](https://semver.org) to manage breaking changes. That means, **major** version +increments (e.g., from `3.x.x` to `4.0.0`) indicate breaking changes. It’s essential that users can rely on **minor** and +**patch** versions (e.g., `3.1.0` or `3.0.1`) being backwards-compatible with the first major release (`3.0.0`). - Breaking changes should be **avoided** whenever possible, as they disrupt users by forcing immediate updates to their - codebase. +When introducing changes, aim to implement them in a non-breaking way. Breaking changes should be **avoided** whenever +possible. If a breaking change is absolutely necessary, strive to transition gradually through **deprecations**. - When introducing changes, aim to implement them in a non-breaking way. If a breaking change is absolutely - necessary, strive to transition gradually through deprecations. +Refer to the [versioning model page](../../installation/versioning-model.md#examples-of-breaking-changes) for some +examples of what constitutes a breaking change. ## What is a deprecation A deprecation signals that a particular part of the code (commonly a function, class, or argument) should no longer be used because it is outdated, inefficient, or replaced by better alternatives. Deprecations are a **temporary** measure -to guide developers toward newer practices, while giving them time to adjust their code without causing immediate -disruptions. +to guide developers toward **transitioning** to newer practices, while giving them time to adjust their code without +causing immediate disruptions. Deprecations act as a soft warning: they indicate that the deprecated feature will eventually be removed, but for now, it remains usable with a runtime deprecation warning. This gives developers enough time to adapt before the removal @@ -34,8 +41,13 @@ takes place in a future major release. It’s essential to understand that deprecations are not permanent — every deprecated feature has a defined removal version, beyond which it will no longer exist. Typically, the removal happens in the next major version after the -deprecation was announced. For example, if a feature is deprecated in version 3.x, it will usually be removed in -version 4.0.0. +deprecation was announced. For example, if a feature is deprecated in version `3.x`, it will usually be removed in +version `4.0.0`. + +!!! info "Recap" + + Deprecations help to avoid **immediate breaking changes** by offering a **grace period** for users to update their + code before the feature is entirely removed in the next major release. Deprecations are primarily used for: @@ -44,6 +56,14 @@ Deprecations are primarily used for: - **Adjusting method signatures**, such as adding required arguments or removing old ones. - **Changing behaviors** that can’t be applied retroactively without introducing errors. +!!! important "Deprecating protocol changes" + + Deprecations are **not used for protocol-related changes**, as the Minecraft protocol evolves independently of + mcproto’s internal development. For these types of changes, mcproto will introduce a major version bump and require + users to update. + + *That said, these changes are still considered as breaking, and will need to be documented as such.* + !!! note Sometimes, it isn't possible/feasible to deprecate something, as the new change is so different from the original diff --git a/docs/index.md b/docs/index.md index 2aceda6e..b3fba637 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,15 @@ Mcproto is a python library that provides various low level interactions with th a full wrapper around the Minecraft protocol, which means it could be used as a basis for Minecraft bots written in python, or even full python server implementations. +!!! important + + Mcproto only covers the **latest minecraft protocol implementation**, updating with each full minecraft release + (not including snapshots!). Using mcproto for older versions of minecraft is not officially supported, if you need + to do so, you will want to use an older version of mcproto, but note that **no bug fixes or features will be + backported** to these older versions. + + *For more information on versioning and update practices, see our [Versioning Practices](./installation/versioning-model.md).* + !!! warning This library is still heavily Work-In-Progress, which means a lot of things can still change and some features may diff --git a/docs/installation/version-guarantees.md b/docs/installation/version-guarantees.md deleted file mode 100644 index 9f1a03e5..00000000 --- a/docs/installation/version-guarantees.md +++ /dev/null @@ -1,38 +0,0 @@ -# Version Guarantees - -!!! danger "Pre-release phase" - - Mcproto is currently in the pre-release phase (pre v1.0.0). During this phase, these guarantees will NOT be - followed! This means that **breaking changes can occur in minor version bumps**. That said, micro version bumps are - still strictly for bugfixes, and will not include any features or breaking changes. - -This library follows [semantic versioning model](https://semver.org), which means the major version is updated every time -there is an incompatible (breaking) change made to the public API. However due to the fairly dynamic nature of Python, -it can be hard to discern what can be considered a breaking change, and what isn't. - -First thing to keep in mind is that breaking changes only apply to **publicly documented functions and classes**. If -it's not listed in the documentation here, it's an internal feature, that isn't considered a part of the public API, -and thus is bound to change. This includes documented attributes that start with an underscore and documented API -that is explicitly marked as internal. - -!!! note - - The examples below are non-exhaustive. - -## Examples of Breaking Changes - -- Changing the default parameter value of a function to something else. -- Renaming (or removing) a function without an alias to the old function. -- Adding or removing parameters of a function. -- Removing deprecated alias to a renamed function. - -## Examples of Non-Breaking Changes - -- Changing function's name, while providing a deprecated alias. -- Renaming (or removing) private underscored attributes. -- Adding an element into `__slots__` of a data class. -- Changing the behavior of a function to fix a bug. -- Changes in the typing definitions of the public API. -- Changes in the documentation. -- Modifying the internal protocol connection handling. -- Updating the dependencies to a newer version, major or otherwise. diff --git a/docs/installation/versioning-model.md b/docs/installation/versioning-model.md new file mode 100644 index 00000000..49914641 --- /dev/null +++ b/docs/installation/versioning-model.md @@ -0,0 +1,105 @@ +# Versioning Practices & Guarantees + +!!! bug "Work In Progress" + + This page is missing an explanation on how to figure out which minecraft version a given mcproto version is for. + This is because we currenly don't have any way to do so, once this will be decided on, it should be documented + here. + +!!! danger "Pre-release phase" + + Mcproto is currently in the pre-release phase (pre v1.0.0). During this phase, these guarantees will NOT be + followed! This means that **breaking changes can occur in minor version bumps**. That said, micro version bumps are + still strictly for bugfixes, and will not include any features or breaking changes. + +This library follows [semantic versioning model](https://semver.org), which means the major version is updated every time +there is an incompatible (breaking) change made to the public API. In addition to semantic versioning, mcproto has +unique versioning practices related to new Minecraft releases. + +## Versioning Model for Minecraft Releases + +Mcproto aims to always be compatible with the **latest Minecraft protocol implementation**, updating the library as +soon as possible after each **full Minecraft release** (snapshots are not supported). + +Typically, a new Minecraft release will result in a major version bump for mcproto, since protocol changes are often +breaking in nature. That said, it is not impossible for a new Minecraft release not to include breaking changes, in +this case, we will not perform this version bump. + +However, there may be cases where we release a major version that does not correspond to a Minecraft update, depending +on the changes made in the library itself. + +!!! info "Recap" + + - **Minecraft Updates**: When a new version of Minecraft is released and introduces breaking changes to the + protocol, mcproto will increment its major version (e.g., from `1.x.x` to `2.0.0`). + - **Non-breaking Protocol Changes**: If a Minecraft update introduces new features or protocol adjustments that do + not break the existing public API, we may opt to release a minor version (e.g., from `1.0.x` to `1.1.0`). + - **Non-protocol Major Releases**: Major releases may also happen due to significant internal changes or + improvements in the library that are independent of Minecraft protocol updates. + +!!! warning + + While mcproto strives to stay updated with Minecraft releases, this project is maintained by unpaid volunteers. We do + our best to release updates in a timely manner after a new Minecraft version, but delays may occur. + +## Examples of Breaking Changes + +First thing to keep in mind is that breaking changes only apply to **publicly documented API**. Internal features, +including any attributes that start with an underscore or those explicitly mentioned as internal are not a part of the +public API and are subject to change without warning. + +Here are examples of what constitutes a breaking change: + +- Changing the default parameter value of a function to something else. +- Renaming (or removing) a function without deprecation +- Adding or removing parameters of a function. +- Removing deprecated alias to a renamed function. +- Protocol changes that affect how public methods or classes behave. + +!!! note + + The examples above are non-exhaustive. + +## Examples of Non-Breaking Changes + +The following changes are considered non-breaking under mcproto’s versioning model: + +
+ +- Changing function's name, while providing a deprecated alias. +- Renaming (or removing) internal attributes or methods, such as those prefixed with an underscore. +- Adding new functionality that doesn’t interfere with existing function signatures or behavior. +- Changing the behavior of a function to fix a bug. (1) +- Changes in the typing definitions of the public API. +- Changes in the documentation. +- Modifying the internal protocol connection handling. +- Adding an element into `__slots__` of a data class. +- Updating the dependencies to a newer version, major or otherwise. + +
+ +1. This only includes changes that don't affect users in a breaking way, unless you're relying on the bug—in which + case, that's on you, and it's probably time to rethink your life choices. + +## Special Considerations + +Given that mcproto is tied closely to the evolving Minecraft protocol, we may have to make breaking changes more +frequently than a typical Python library. + +While we aim to provide deprecation warnings for changes, particularly in **protocol-independent core library +features**, +there are certain limitations due to the nature of Minecraft protocol updates. When a major update is released as a +result of a Minecraft protocol change, **we will not provide deprecations for affected features**, as the protocol itself +has changed in a way that necessitates immediate adaptation. + +However, for **internal major updates** that are independent of Minecraft protocol changes, **we will make every effort +to deprecate old behavior**, giving users time to transition smoothly before removing legacy functionality. + +## Communicating deprecations + +When a feature is deprecated, we will notify users through: + +- **Warnings in the code** (via `DeprecationWarning`): These warnings will contain details about what was deprecated, + including a replacement option (if there is one) and a version number for when this deprecation will be removed. +- **Detailed notes in the release changelog**: This includes any migration instructions and a brief reason for + deprecation. diff --git a/mkdocs.yml b/mkdocs.yml index bba33890..1a8f00ac 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,7 +15,7 @@ nav: - Home: index.md - Installation: - Installation: installation/index.md - - Version Guarantees: installation/version-guarantees.md + - Versioning Model: installation/versioning-model.md - Changelog: installation/changelog.md - Usage: - usage/index.md From bfc21d1dcfd229e75e8db35b7ebf74e58092da86 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 24 Oct 2024 23:42:38 +0200 Subject: [PATCH 69/85] Update the ordering of some contributing guides --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 1a8f00ac..a9850b5b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,10 +37,10 @@ nav: - Slotscheck: contributing/guides/slotscheck.md - Pre-commit: contributing/guides/precommit.md - Changelog: contributing/guides/changelog.md - - Great commits: contributing/guides/great-commits.md - - Unit Tests: contributing/guides/unit-tests.md - Breaking Changes: contributing/guides/breaking-changes.md + - Unit Tests: contributing/guides/unit-tests.md - Documentation: contributing/guides/documentation.md + - Great commits: contributing/guides/great-commits.md - API Reference: - Protocol: reference/public/protocol.md - Authentication: reference/public/authentication.md From 479d821f237123da544d57d5dab6c37c89eaa02b Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 26 Oct 2024 13:34:40 +0200 Subject: [PATCH 70/85] Move usage guides from readme to docs --- README.md | 332 +---------------------------- docs/usage/first-steps.md | 249 ++++++++++++++++++++++ docs/usage/packet-communication.md | 187 ++++++++++++++++ mkdocs.yml | 2 + 4 files changed, 446 insertions(+), 324 deletions(-) create mode 100644 docs/usage/first-steps.md create mode 100644 docs/usage/packet-communication.md diff --git a/README.md b/README.md index ac129f89..8155532d 100644 --- a/README.md +++ b/README.md @@ -5,330 +5,14 @@ [![current PyPI version](https://img.shields.io/pypi/v/mcproto.svg)](https://pypi.org/project/mcproto/) [![Validation](https://github.com/ItsDrike/mcproto/actions/workflows/validation.yml/badge.svg)](https://github.com/ItsDrike/mcproto/actions/workflows/validation.yml) [![Unit tests](https://github.com/ItsDrike/mcproto/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/ItsDrike/mcproto/actions/workflows/unit-tests.yml) -[![Docs](https://img.shields.io/readthedocs/mcproto?label=Docs)](https://mcproto.readthedocs.io/) +[![Docs](https://github.com/py-mine/mcproto/actions/workflows/mkdocs.yml/badge.svg))](https://py-mine.github.io/mcproto) -This is a heavily Work-In-Progress library, which attempts to be a full wrapper around the minecraft protocol, allowing -for simple interactions with minecraft servers, and perhaps even for use as a base to a full minecraft server -implementation in python (though the speed will very likely be quite terrible, making it probably unusable as any -real-world playable server). +Mcproto is a python library that provides various low level interactions with the Minecraft protocol. It attempts to be +a full wrapper around the Minecraft protocol, which means it could be used as a basis for Minecraft bots written in +python, or even full python server implementations. -Currently, the library is very limited and doesn't yet have any documentation, so while contributions are welcome, fair -warning that there is a lot to comprehend in the code-base and it may be challenging to understand it all. +> [!WARNING] +> Currently, the library is still work in progress and very incomplete, so while contributions are welcome, fair warning +> that using mcproto in production isn't recommended. -## Examples - -Since there is no documentation, to satisfy some curious minds that really want to use this library even in this -heavily unfinished state, here's a few simple snippets of it in practice: - -### Manual communication with the server - -As sending entire packets is still being worked on, the best solution to communicate with a server is to send the data -manually, using our connection reader/writer, and buffers (being readers/writers, but only from bytearrays as opposed -to using an actual connection). - -Fair warning: This example is pretty long, but that's because it aims to explain the minecraft protocol to people that -see it for the first time, and so a lot of explanation comments are included. But the code itself is actually quite -simple, due to a bunch of helpful read/write methods the library already provides. - -```python -import json -import asyncio - -from mcproto.buffer import Buffer -from mcproto.connection import TCPAsyncConnection -from mcproto.protocol.base_io import StructFormat - - -async def handshake(conn: TCPAsyncConnection, ip: str, port: int = 25565) -> None: - # As a simple example, let's request status info from a server. - # (this is what you see in the multiplayer server list, i.e. the server's motd, icon, info - # about how many players are connected, etc.) - - # To do this, we first need to understand how are minecraft packets composed, and take a look - # at the specific packets that we're interested in. Thankfully, there's an amazing community - # made wiki that documents all of this! You can find it at https://wiki.vg/ - - # Alright then, let's take a look at the (uncompressed) packet format specification: - # https://wiki.vg/Protocol#Packet_format - # From the wiki, we can see that a packet is composed of 3 fields: - # - Packet length (in bytes), sent as a variable length integer - # combined length of the 2 fields below - # - Packet ID, also sent as varint - # each packet has a unique number, that we use to find out which packet it is - # - Data, specific to the individual packet - # every packet can hold different kind of data, this will be shown in the packet's - # specification (you can find these in wiki.vg) - - # Ok then, with this knowledge, let's establish a connection with our server, and request - # status. To do this, we fist need to send a handshake packet. Let's do it: - - # Let's take a look at what data the Handshake packet should contain: - # https://wiki.vg/Protocol#Handshake - handshake = Buffer() - # We use 47 for the protocol version, as it's quite old, and will work with almost any server - handshake.write_varint(47) - handshake.write_utf(ip) - handshake.write_value(StructFormat.USHORT, port) - handshake.write_varint(1) # Intention to query status - - # Nice! Now that we have the packet data, let's follow the packet format and send it. - # Let's prepare another buffer that will contain the last 2 fields (packet id and data) - # combined. We do this since the first field will require us to know the size of these - # two combined, so let's just put them into 1 buffer. - packet = Buffer() - packet.write_varint(0) # Handshake packet has packet ID of 0 - packet.write(handshake) # Full data from our handshake packet - - # And now, it's time to send it! - await conn.write_varint(len(packet)) # First field (size of packet id + data) - await conn.write(packet) # Second + Third fields (packet id + data) - - -async def status(conn: TCPAsyncConnection, ip: str, port: int = 25565) -> dict: - # This function will be called right after a handshake - # Sending this packet told the server recognize our connection, and since we've specified - # the intention to query status, it then moved us to STATUS game state. - - # Different game states have different packets that we can send out, for example there is a - # game state for login, that we're put into while joining the server, and from it, we tell - # the server our username player UUID, etc. - - # The packet IDs are unique to each game state, so since we're now in status state, a packet - # with ID of 0 is no longer the handshake packet, but rather the status request packet - # (precisely what we need). - # https://wiki.vg/Protocol#Status_Request - - # The status request packet is empty, and doesn't contain any data, it just instructs the - # server to send us back a status response packet. Let's send it! - packet = Buffer() - packet.write_varint(0) # Status request packet ID - - await conn.write_varint(len(packet)) - await conn.write(packet) - - # Now, let's receive the response packet from the server - # Remember, the packet format states that we first receive a length, then packet id, then data - _response_len = await conn.read_varint() - _response = await conn.read(_response_len) # will give us a bytearray - - # Amazing, we've just received data from the server! But it's just bytes, let's turn it into - # a Buffer object, which includes helpful methods that allow us to read from it - response = Buffer(_response) - packet_id = response.read_varint() # Remember, 2nd field is the packet ID - - # Let's see it then, what packet did we get? - print(packet_id) # 0 - - # Interesting, this packet has an ID of 0, but wasn't that the status request packet? We wanted - # a response tho. Well, actually, if we take a look at the status response packet at the wiki, - # it really has an ID of 0: - # https://wiki.vg/Protocol#Status_Response - # Aha, so not only are packet ids unique between game states, they're also unique between the - # direction a server bound packet (sent by client, with server as the destination) can have an - # id of 0, while a client bound packet (sent by server, with client as the destination) can - # have the same id, and mean something else. - - # Alright then, so we know what we got is a status response packet, let's read the wiki a bit - # further and see what data it actually contains, and see how we can get it out. Hmmm, it - # contains a UTF-8 encoded string that represents JSON data, ok, so let's get that string, it's - # still in our buffer. - received_string = response.read_utf() - - # Now, let's just use the json module, convert the string into some json object (in this case, - # a dict) - data = json.loads(received_string) - return data - -async def main(): - # That's it, all that's left is actually calling our functions now - - ip = "mc.hypixel.net" - port = 25565 - - async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: - await handshake(connection, ip, port) - data = await status(connection, ip, port) - - # Wohoo, we got the status data! Let's see it - print(data["players"]["max"]) # This is the server's max player amount (slots) - print(data["players"]["online"]) # This is how many people are currently online - print(data["description"]) # And here's the motd - - # There's a bunch of other things in this data, try it out, see what you can find! - -def start(): - # Just some boilerplate code that can run our asynchronous main function - asyncio.run(main()) -``` - -### Using packet classes for communication - -The first thing you'll need to understand about packet classes in mcproto is that they're generally going to support -the latest minecraft version, and while any the versions are usually mostly compatible, mcproto does NOT guarantee -support for any older protocol versions. - -#### Obtaining the packet map - -As we've already seen in the example before, packets follow certain format, and every packet has it's associated ID -number, direction (client->server or server->client), and game state (status/handshaking/login/play). The packet IDs -are unique to given direction and game state combination. - -For example in clientbound direction (packets sent from server to the client), when in the status game state, there -will always be unique ID numbers for the different packets. In this case, there would actually only be 2 packets here: -The Ping response packet, which has an ID of 1, and the Status response packet, with an ID of 0. - -To receive a packet, we therefore need to know both the game state, and the direction, as only then are we able to -figure out what the type of packet it is. In mcproto, packet receiving therefore requires a "packet map", which is a -mapping (dictionary) of packet id -> packet class. Here's an example of obtaining a packet map: - -```python -from mcproto.packets import generate_packet_map, GameState, PacketDirection - -STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) -``` - -Which, if you were to print it, would look like this: - -``` -{ - 0: - 1: , -} -``` - -Telling us that in the status gamestate, for the clientbound direction, these are the only packet we can receive, -and showing us the actual packet classes for every possible ID number. - -#### Building our own packets - -Our first packet will always have to be a Handshake, this is the only packet in the entire handshaking state, and it's -a "gateway", after which we get moved to a different state, specifically, either to STATUS (to obtain information about -the server, such as motd, amount of players, or other details you'd see in the multiplayer screen in your MC client). - -```python -from mcproto.packets.handshaking.handshake import Handshake, NextState - -my_handshake = Handshake( - # Once again, we use an old protocol version so that even older servers will respond - protocol_version=47, - server_address="mc.hypixel.net", - server_port=25565, - next_state=NextState.STATUS, -) -``` - -That's it! We've now constructed a full handshake packet with all of the data it should contain. You might remember -from the example above, that we originally had to look at the protocol specification, find the handshake packet and -construct it's data as a Buffer with all of these variables. - -With these packet classes, you can simply follow your editor's autocompletion to see what this packet requires, pass it -in and the data will be constructed for you from these attributes, without constantly cross-checking with the wiki. - -For completion, let's also construct the status request packet that we were sending to instruct the server to send us -back the status response packet. - -```python -from mcproto.packets.status.status import StatusRequest - -my_status_request = StatusRequest() -``` - -This one was even easier, as the status request packet alone doesn't contain any special data, it's just a request to -the server to send us some data back. - -#### Sending packets - -To actually send out a packet to the server, we'll need to create a connection, and use the custom functions -responsible for sending packets out. Let's see it: - -```python -from mcproto.packets import async_write_packet -from mcproto.connection import TCPAsyncConnection - -async def main(): - ip = "mc.hypixel.net" - port = 25565 - - async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: - await async_write_packet(connection, my_handshake) - # my_handshake is a packet we've created in the example before -``` - -Much easier than the manual version, isn't it? - -#### Receiving packets - -Alright, we might now know how to send a packet, but how do we receive one? Let's see: - -```python -# Let's say we already have a connection at this moment, after all, how else would -# we've gotten into the STATUS game state. -# Also, let's do something different, let's say we have a synchronous connection, just for fun -from mcproto.connection import TCPSyncConnection -conn: TCPSyncConnection - -# With a synchronous connection, comes synchronous reader, so instead of using async_read_packet, -# we'll use sync_read_packet here -from mcproto.packets import sync_read_packet - -# But remember? To read a packet, we'll need to have that packet map, telling us which IDs represent -# which actual packet types. Let's pass in the one we've constructed before -packet = sync_read_packet(conn, STATUS_CLIENTBOUND_MAP) - -# Cool! We've got back a packet, let's see what kind of packet we got back -from mcproto.packets.status.status import StatusResponse -from mcproto.packets.status.ping import PingPong - -if isinstance(packet, StatusResponse): - ... -elif isinstance(packet, PingPong): - ... -else: - raise Exception("Impossible, there aren't other client bound packets in the STATUS game state") -``` - -#### Requesting status - -Alright, so let's actually try to put all of this knowledge together, and create something meaningful. Let's replicate -the status obtaining logic from the manual example, but with these new packet classes: - -```python -from mcproto.connection import TCPAsyncConnection -from mcproto.packets import async_write_packet, async_read_packet, generate_packet_map -from mcproto.packets.packet import PacketDirection, GameState -from mcproto.packets.handshaking.handshake import Handshake, NextState -from mcproto.packets.status.status import StatusRequest, StatusResponse -from mcproto.packets.status.ping import PingPong - -STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) - - -async def get_status(ip: str, port: int) -> dict: - handshake_packet = Handshake( - protocol_version=47, - server_address=ip, - server_port=port, - next_state=NextState.STATUS, - ) - status_req_packet = StatusRequest() - - async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: - # We start out at HANDSHAKING game state - await async_write_packet(connection, handshake_packet) - # After sending the handshake, we told the server to now move us into the STATUS game state - await async_write_packet(connection, status_req_packet) - # Since we're still in STATUS game state, we use the status packet map when reading - packet = await async_read_packet(connection, STATUS_CLIENTBOUND_MAP) - - # Now that we've got back the packet, we no longer need the connection, we won't be sending - # anything else, so let's get out of the context manager. - - # Now, we should always first make sure it really is the packet we expected - if not isinstance(packet, StatusResponse): - raise ValueError(f"We've got an unexpected packet back: {packet!r}") - - # Since we know we really are dealing with a status response, let's get out it's data, and return it - return packet.data -``` - -Well, that wasn't so hard, was it? +For more info, check our our [documentation](https://py-mine.github.io/mcproto). diff --git a/docs/usage/first-steps.md b/docs/usage/first-steps.md new file mode 100644 index 00000000..c0e8a816 --- /dev/null +++ b/docs/usage/first-steps.md @@ -0,0 +1,249 @@ +# Manual communication with the server + +This example demonstrates how to interact with a Minecraft server using mcproto at it's lowest-level interface. It +avoids the built-in packet classes to show how to manually handle data through mcproto's connection and buffer classes. +Although this isn’t the typical use case for mcproto, it provides insight into the underlying Minecraft protocol, which +is crucial to understand before transitioning to using the higher-level packet handling. + +In this example, we'll retrieve a server's status — information displayed in the multiplayer server list, such as the +server's MOTD, icon, and player count. + +## Step-by-step guide + +### Handshake with the server + +The first step when doing pretty much any kind of communication with the server is establishing a connection and +sending a "handshake" packet. + +??? question "What even is a packet?" + + A packet is a structured piece of data sent across a network to encode an action or message. In games, packets + allow different kinds of information — such as a player's movement, an item pickup, or a chat message — to be + communicated in a structured way, with each packet tailored for a specific purpose. + + Every packet has a set structure with fields that identify it and hold its data, making it clear what action or + event the packet is meant to represent. While packets may carry different types of information, they usually follow + a similar format, so the game’s client and server can read and respond to them easily. + +To do this, we first need to understand Minecraft packets structure in general, then focus on the specific handshake +packet format. To find this out, we recommend using [wiki.vg](https://wiki.vg), which is a fantastic resource, +detailing all of the Minecraft protocol logic. + +So, according to the [Packet Format](https://wiki.vg/Protocol#Packet_format) page, a Minecraft packet has three fields: + +- **Packet length**: the total size of the Packet ID and Data fields (in bytes). Sent in a variable length integer + format. +- **Packet ID**: uniquely identifies which packet this is. Also sent in the varint format. +- **Data**: the packet's actual content. This will differ depending on the packet type. + +Another important information to know is that Minecraft protocol operates in “states,” each with its own set of packets +and IDs. For example, the same packet ID in one state may represent a completely different packet in another state. +Upon establishing a connection with a Minecraft server, you'll begin in the "handshaking" state, with only one packet +available: the handshake packet. This packet tells the server which state to enter next. + +In our case, we’ll request to enter the "status" state, used for obtaining server information (in contrast, the "login" +state would be used to join the server). + +Next, let’s look at the specifics of the handshake packet on wiki.vg [here](https://wiki.vg/Protocol#Handshake). + +From here, we can see that the handshake packet has an ID of `0` and should contain the following data (fields): + +- **Protocol Version**: The version of minecraft protocol (for compatibility), sent as a varint. +- **Server Address**: The hostname or IP that was used to connect to the server, sent as a string with max length of + 255 characters. +- **Server Port**: The port number (usually 25565), sent as unsigned short. +- **Next State**: The desired state to transition to, sent as a varint. (1 for "status".) + +Armed with this information, we can start writing code to send the handshake: + +```python +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection +from mcproto.protocol.base_io import StructFormat + + +async def handshake(conn: TCPAsyncConnection, ip: str, port: int = 25565) -> None: + handshake = Buffer() + # We use 47 for the protocol version, as which is quite old. We do that to make sure that this code + # will work with almost any server, including older ones. Using a newer protocol number may result + # in older servers refusing to respond. + handshake.write_varint(47) + handshake.write_utf(ip) + handshake.write_value(StructFormat.USHORT, port) + handshake.write_varint(1) # The next state should be "status" + + # Nice! Now we have the packet data, stored in a buffer object. + # This is the data field in the packet format specification. + + # Let's prepare another buffer that will contain the last 2 packet format fields (packet id and data). + # We do this since the first field will require us to know the size of these two combined, + # so let's put them into 1 buffer first: + packet = Buffer() + packet.write_varint(0) # Handshake packet ID + packet.write(handshake) # The entire handshake data, from our previous buffer. + + # And finally, it's time to send it! + await conn.write_varint(len(packet)) # First field (size of packet id + data) + await conn.write(packet) # Second + Third fields (packet id + data) +``` + +### Running the code + +Now, you may be wondering how to actually run this code, what is `TCPAsyncConnection`? Essentially, it's just a wrapper +around a socket connection, designed specifically for communication with Minecraft servers. + +To create an instance of this connection, you'll want to use an `async with` statement, like so: + +```python +import asyncio + +from mcproto.connection import TCPAsyncConnection + +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + await handshake(connection, ip, port) + +def start(): + # Just some boilerplate code that we can run our asynchronous main function + asyncio.run(main()) +``` + +Currently, this code only establishes a connection and requests a state transition to "status", so when running it you +won't see any meaningful result just yet. + +!!! tip "Synchronous handling" + + Even though we're using asynchronous connection in this example, mcproto does also provide a synchronous + version: `TCPSyncConnection`. + + While you can use this synchronous option, we recommend the asynchronous approach as it highlights blocking + operations with the `await` keyword and allows other tasks to run concurrently, while these blocking operations are + waiting. + +### Obtaining server status + +Now comes the interesting part, we'll request a status from the server, and read the response that it sends us. Since +we're already in the status game state by now, we'll want to take a look at the packets that are available in this +state. Once again, wiki.vg datails all of this for us [here](). + +We can notice that the packets are split into 2 categories: **client-bound** and **server-bound**. We'll first want to +look at the server-bound ones (i.e. packets targetted to the server, sent by the client - us). There are 2 packets +listed here: Ping Request and Status request. Ping is only here to check if the server is online, and allow us to +measure how long the response took, getting the latency, we're not that interested in doing this now, we want to see +some actual useful data from the server, so we'll choose the Status request packet. + +Since this packet just tells the server to send us the status, it actually doesn't contain any data fields for us to +add, so the packet itself will be empty: + +```python +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection + +async def status_request(conn: TCPAsyncConnection) -> None: + # Let's construct a buffer with the packet ID & packet data (like we saw in the handshake example already) + # However, since the status request packet doesn't contain any data, we just need to set the packet id. + packet = Buffer() + packet.write_varint(0) # Status request packet ID + + await conn.write_varint(len(packet)) + await conn.write(packet) +``` + +After we send this request, the server should respond back to us. But what will it respond with? Well, let's find out: + +```python +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection + +async def read_status_response(conn: TCPAsyncConnection) -> None: + # Remember, the packet format states that we first receive a length, then packet id, then data + _response_len = await conn.read_varint() + _response = await conn.read(_response_len) # will give us a bytearray + + # Amazing, we've just received data from the server! But it's just bytes, let's turn it into + # a Buffer object, which includes helpful methods that allow us to read from it + response = Buffer(_response) + packet_id = response.read_varint() # Remember, 2nd field is the packet ID, encoded as a varint + + print(packet_id) +``` + +Adjusting our main function to run the new logic: + +```python +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + await handshake(connection, ip, port) + await status_request(connection) + await read_status_response(connection) +``` + +Running the code now, we can see it print `0`. Aha! That's our packet ID, so let's see what the server sent us. So, +looking through the list of **client-bound** packets in the wiki, this is the **Status Response Packet**! + +!!! note + + Interesting, this packet has an ID of 0, wasn't that the status request packet? + + Indeed, packets can have the same ID in different directions, so packet ID `0` for a client-bound response is + distinct from packet ID `0` for a server-bound request. + +Alright then, let's see what the status response packet contains: The wiki says it just has a single UTF-8 string +field, which contains JSON data. Let's adjust our function a bit, and read that data: + +```python +import json + +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection + +async def read_status_response(conn: TCPAsyncConnection) -> dict: # We're now returning a dict + _response_len = await conn.read_varint() + _response = await conn.read(_response_len) + + response = Buffer(_response) + packet_id = response.read_varint() + + # Let's always make sure we got the status response packet here. + assert packet_id == 0 + + # Let's now read that single UTF8 string field, it should still be in our buffer: + received_string = response.read_utf() + + # Now, let's just use the json built-in library, convert the JSON string into a python object + # (in this case, it will be a dict) + data = json.loads(received_string) + + # Cool, we now have the actual status data that the server has provided, we should return them + # from the function now. + # Before we do that though, let's just do a sanity-check and ensure that the buffer doesn't contain + # any more data. + assert response.remaining == 0 # 0 bytes (everything was read) + return data +``` + +Finally, we'll adjust the main function to show some of the status data that we obtained: + +```python +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + await handshake(connection, ip, port) + await status_request(connection) + data = await read_status_response(connection) + + # Wohoo, we got the status data! Let's see it + print(data["players"]["max"]) # This is the server's max player amount (slots) + print(data["players"]["online"]) # This is how many people are currently online + print(data["description"]) # And here's the motd + + # There's a bunch of other things in this data, try it out, see what you can find! +``` diff --git a/docs/usage/packet-communication.md b/docs/usage/packet-communication.md new file mode 100644 index 00000000..b35203a0 --- /dev/null +++ b/docs/usage/packet-communication.md @@ -0,0 +1,187 @@ +# Packet communication + +This guide explains how to communicate with the server using our packet classes. It will go over the same example from +[previous page](./first-steps.md), showing how to obtain the server status, but instead of using the low level +interactions, this guide will simplify a lot of that logic with the use of packet classes. + +!!! warning "Packets Target the Latest Minecraft Version" + + Mcproto's packet classes are designed to support the **latest Minecraft release**. While packets in the handshaking + and status game states usually remain compatible across versions, mcproto does NOT guarantee cross-version packet + compatibility. Using packets in the play game state, for example, will very likely lead to compatibility issues if + you're working with older Minecraft versions. + + Only the low level interactions are guaranteed to remain compatible across protocol updates, if you need support + for and older minecraft version, consider downgrading to an older version of mcproto, or using the low level + interactions. + +## Obtaining the packet map + +Every packet has a unique ID based on its direction (client to server or server to client) and game state (such as +status, handshaking, login, or play). This ID lets us recognize packet types in different situations, which is crucial +for correctly receiving packets. + +To make this process easier, mcproto provides a packet map—essentially a dictionary mapping packet IDs to packet +classes. Here’s how to generate a packet map: + +```python +from mcproto.packets import generate_packet_map, GameState, PacketDirection + +STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) +``` + +Printing `STATUS_CLIENTBOUND_MAP` would display something like this: + +``` +{ + 0: + 1: , +} +``` + +Telling us that in the STATUS gamestate, for the clientbound direction, these are the only packet we can receive, +and mapping the actual packet classes for every supported packet ID number. + +## Using packets + +The first packet we send to the server is always a **Handshake** packet. This is the only packet in the entire +handshaking state, and it's a "gateway", after which we get moved to a different state, in our case, that will be the +STATUS state. + +```python +from mcproto.packets.handshaking.handshake import Handshake, NextState + +my_handshake = Handshake( + # Once again, we use an old protocol version so that even older servers will respond + protocol_version=47, + server_address="mc.hypixel.net", + server_port=25565, + next_state=NextState.STATUS, +) +``` + +That's it! We've now constructed a full handshake packet with all of the data it should contain. You might remember +from the previous low-level example, that we originally had to look at the protocol specification, find the handshake +packet and construct it's data as a Buffer with all of these variables. + +With these packet classes, you can simply follow your editor's autocompletion to see what this packet requires, pass it +in and the data will be constructed for you from these attributes, without constantly cross-checking with the wiki. + +For completion, let's also construct the status request packet that we were sending to instruct the server to send us +back the status response packet. + +```python +from mcproto.packets.status.status import StatusRequest + +my_status_request = StatusRequest() +``` + +This one was even easier, as the status request packet alone doesn't contain any special data, it's just a request to +the server to send us some data back. + +## Sending packets + +To actually send out a packet to the server, we'll need to create a connection, and use mcproto's `async_write_packet` +function, responsible for sending packets. Let's see it: + +```python +from mcproto.packets import async_write_packet +from mcproto.connection import TCPAsyncConnection + +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), timeout=2)) as connection: + # Let's send the handshake packet that we've created in the example before + await async_write_packet(connection, my_handshake) + # Followed by the status request + await async_write_packet(connection, my_status_request) +``` + +Much easier than the manual version, isn't it? + +## Receiving packets + +Alright, we might now know how to send a packet, but how do we receive one? + +Let's see, but this time, let's also try out using the synchronous connection, just for fun: + +```python +from mcproto.connection import TCPSyncConnection + +# With a synchronous connection, comes synchronous reader/writer functions +from mcproto.packets import sync_read_packet, sync_write_packet + +# We'll also need the packet classes from the status game-state +from mcproto.packets.status.status import StatusResponse +from mcproto.packets.status.ping import PingPong + +def main(): + ip = "mc.hypixel.net" + port = 25565 + + with TCPSyncConnection.make_client(("mc.hypixel.net", 25565), 2) as conn: + # First, send the handshake & status request, just like before, but synchronously + await sync_write_packet(connection, my_handshake) + await sync_write_packet(connection, my_status_request) + + # To read a packet, we'll also need to have the packet map, telling us which IDs represent + # which actual packet types. Let's pass in the map that we've constructed before: + packet = sync_read_packet(conn, STATUS_CLIENTBOUND_MAP) + + # Now that we've got back the packet, we no longer need the connection, we won't be sending + # anything else, so let's get out of the context manager. + + # Finally, let's handle the received packet: + if isinstance(packet, StatusResponse): + ... + elif isinstance(packet, PingPong): + ... + else: + raise Exception("Impossible, there are no other client bound packets in the STATUS game state") +``` + +## Requesting status + +Alright, so let's actually try to put all of this knowledge together, and create something meaningful. Let's replicate +the status obtaining logic from the manual example, but with these new packet classes: + +```python +from mcproto.connection import TCPAsyncConnection +from mcproto.packets import async_write_packet, async_read_packet, generate_packet_map +from mcproto.packets.packet import PacketDirection, GameState +from mcproto.packets.handshaking.handshake import Handshake, NextState +from mcproto.packets.status.status import StatusRequest, StatusResponse + +STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) + + +async def get_status(ip: str, port: int) -> dict: + handshake_packet = Handshake( + protocol_version=47, + server_address=ip, + server_port=port, + next_state=NextState.STATUS, + ) + status_req_packet = StatusRequest() + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + # We start out at HANDSHAKING game state + await async_write_packet(connection, handshake_packet) + # After sending the handshake, we told the server to now move us into the STATUS game state + await async_write_packet(connection, status_req_packet) + # Since we're still in STATUS game state, we use the status packet map when reading + packet = await async_read_packet(connection, STATUS_CLIENTBOUND_MAP) + + # Now, we should always first make sure it really is the packet we expected + if not isinstance(packet, StatusResponse): + raise ValueError(f"We've got an unexpected packet back: {packet!r}") + + # Since we know we really are dealing with a status response, let's get out it's data, and return it + # this is the same JSON data that we obtained from the first example with the manual interactions + return packet.data +``` + +As you can see, this approach is more convenient and eliminates much of the manual packet handling, letting you focus +on higher-level logic! diff --git a/mkdocs.yml b/mkdocs.yml index a9850b5b..26c2c308 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,8 @@ nav: - Changelog: installation/changelog.md - Usage: - usage/index.md + - First steps: usage/first-steps.md + - Packet Communication: usage/packet-communication.md - Authentication: usage/authentication.md - Community: - Code of Conduct: community/code-of-conduct.md From 8d36d6adb3954d1b1c112be5c2cef6fcc1481368 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 26 Oct 2024 18:13:13 +0200 Subject: [PATCH 71/85] Mention which packages will/won't get deprecated explicitly --- docs/installation/versioning-model.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/installation/versioning-model.md b/docs/installation/versioning-model.md index 49914641..98587fd5 100644 --- a/docs/installation/versioning-model.md +++ b/docs/installation/versioning-model.md @@ -87,19 +87,25 @@ Given that mcproto is tied closely to the evolving Minecraft protocol, we may ha frequently than a typical Python library. While we aim to provide deprecation warnings for changes, particularly in **protocol-independent core library -features**, -there are certain limitations due to the nature of Minecraft protocol updates. When a major update is released as a -result of a Minecraft protocol change, **we will not provide deprecations for affected features**, as the protocol itself -has changed in a way that necessitates immediate adaptation. +features**, there are certain limitations due to the nature of Minecraft protocol updates. When a major update is +released as a result of a Minecraft protocol change, **we will not provide deprecations for affected features**, as the +protocol itself has changed in a way that necessitates immediate adaptation. However, for **internal major updates** that are independent of Minecraft protocol changes, **we will make every effort to deprecate old behavior**, giving users time to transition smoothly before removing legacy functionality. -## Communicating deprecations +Specifically, the protocol dependant code includes code in `mcproto.packets` and `mcproto.types` packages. Lower level +protocol abstractions present in `mcproto.protocol`, `mcproto.buffer`, `mcproto.connection`, `mcproto.encryption`, +`mcproto.multiplayer` and `mcproto.auth` will go through proper deprecations. This should allow you to safely use these +lower level features to communicate to servers at any protocol version. + +## Communicating deprecations & breaking changes + +When a breaking change occurs, you will always find it listed at the top of the changelog. Here, will also find +detailed notes about any migration instructions and a brief reason for the change. When a feature is deprecated, we will notify users through: - **Warnings in the code** (via `DeprecationWarning`): These warnings will contain details about what was deprecated, including a replacement option (if there is one) and a version number for when this deprecation will be removed. -- **Detailed notes in the release changelog**: This includes any migration instructions and a brief reason for - deprecation. +- **Entries in the changelog**: This includes any migration instructions and a brief reason for deprecation. From dbf4332182a1b6f8f56d4d85822616034681c133 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 26 Oct 2024 18:20:00 +0200 Subject: [PATCH 72/85] Fix various typos and wording --- docs/contributing/guides/great-commits.md | 19 ++++++++++--------- docs/contributing/guides/index.md | 2 +- docs/contributing/making-a-pr.md | 14 +++++++------- docs/contributing/reporting-a-bug.md | 4 ++-- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/contributing/guides/great-commits.md b/docs/contributing/guides/great-commits.md index 3442e3ba..926ea90f 100644 --- a/docs/contributing/guides/great-commits.md +++ b/docs/contributing/guides/great-commits.md @@ -6,8 +6,8 @@ over the `git log`, or `git blame`. It explains the purpose of a commit message and it's structure, goes over the importance of making commits - "atomic" and the practice of partial staging, mentions why and how to avoid making a lot of fixing commits, - describes force pushing after modifying the git history, alongside it's downsides and finally, it explains why + "atomic" and the practice of partial staging. Additionally, it also mentions why and how to avoid making a lot of + fixing commits, describes the practice of force pushing, alongside it's downsides and finally, it explains why these practices are worth following and how they make the developer's life easier. A well-structured git log is crucial for a project's maintainability, providing insight into changes as a reference for @@ -61,7 +61,7 @@ Git commits should be written in a very specific way. There’s a few rules to f 1. **Subject Line:** - **Limit to 50 characters** (This isn't a hard limit, but try not to go much longer. This limit ensures - readability and forces the author to thing about the most concise way to explain what's going on. Hint: If you're + readability and forces the author to think about the most concise way to explain what's going on. Hint: If you're having trouble summarizing, you might be committing too much at once) - **A single sentence** (The summary should be a single sentence, multiple probably wouldn't fit into the character limit anyways) @@ -153,16 +153,17 @@ pretty well, and while sometimes this is the case, in many cases, you might've a noticed while working on your changes, or already implemented something else, that doesn't fit into your single atomic commit that you now wish to make. -In this case, it can be very useful to know that you can instead make a "partial" add, and only stage those changes -that belong to the commit. In some cases, all that you'll need is to only stage some specific files, which you can do -with: +In this case, it can be very useful to know that you can instead make a "partial" add, only staging those changes that +belong to the commit. + +In some cases, it will be sufficient to simpy stage specific files, which you can do with: ```bash git add path/to/some/file path/to/other/file ``` -That said, in most cases, you're left with a single file that contains multiple changes. When this happens, you can use -the `-p`/`--patch` flag: +That said, in most cases, you're left with a single file that contains multiple unrelated changes. When this happens, +you can use the `-p`/`--patch` flag: ```bash git add -p path/to/file @@ -220,7 +221,7 @@ your branch to the remote (to GitHub) regardless of what was in the remote alrea Force pushing becomes risky if others have already pulled the branch you are working on. If you overwrite the branch with a force push, it can lead to several issues: - - **Lost Work:** Collaborators may have pushed to your branch already, following it's existing git history. + - **Lost work:** Collaborators may have pushed to your branch already, following it's existing git history. However, after your force-push, their changes would be ereased from the remote. **Make sure you pull / rebase from the remote before you make a force-push.** - **Complex conflicts:** If someone else has pulled your branch and did some changes that they didn't yet push diff --git a/docs/contributing/guides/index.md b/docs/contributing/guides/index.md index 96711e41..2dc274f0 100644 --- a/docs/contributing/guides/index.md +++ b/docs/contributing/guides/index.md @@ -5,7 +5,7 @@ interested in writing or modifying mcproto itself. If you just wish to use mcpro this section. Mcproto is a relatively large project and maintaining it is no easy task. With a project like that, consistency and -good code quality becomes very important to keep the code-base readable and bug-free. To achieve this, we have put +good code quality become very important to keep the code-base readable and bug-free. To achieve this, we have put together these guidelines that will explain the code style and coding practices that we expect our contributors to follow. diff --git a/docs/contributing/making-a-pr.md b/docs/contributing/making-a-pr.md index 43a018e0..923d706f 100644 --- a/docs/contributing/making-a-pr.md +++ b/docs/contributing/making-a-pr.md @@ -21,17 +21,17 @@ repository. Contributions can include bug fixes, documentation updates, or new f The very first thing you will need to do is deciding what you actually want to work on. In all likelihood, you already have something in mind if you're reading this, however, if you don't, you're always free to check the opened GitHub -issues. If you find anything interesting there that you'd wish to work on, leave a comment on that issue with something -like: "I'd like to work on this". +issues, that don't yet have anyone assigned. If you find anything interesting there that you'd wish to work on, leave a +comment on that issue with something like: "I'd like to work on this". Even if you do have an idea already, we heavily recommend (though not require) that you first make an issue, this can be a [bug report](./reporting-a-bug.md), but also a feature request, or something else. Once you made the issue, leave a: "I'd like to work on this" comment on it. -Eventually, a maintainer will get back to you and you will be assigned to the issue. By getting assigned, you reserve -the right to work on that given issue and it also prevents us (or someone else) from potentially working on the same -thing that you're already addressing. This is also the reason why we recommend creating an issue first. Being assigned -is a soft approval from us, giving you the green light to start coding. +Eventually, a maintainer will get back to you and you will be assigned to the issue. Being assigned is a soft approval +from us, giving you the green light to start coding. By getting assigned, you also reserve the right to work on that +given issue, hence preventing us (or someone else) from potentially working on the same thing, wasting ours or your +time. This prevention of duplicate efforts is also the primary reason why we recommend creating an issue first. Of course, you are welcome to start working on the issue even before being officially assigned. However, please be aware that sometimes, we may choose not to pursue a certain feature / bugfix. In such cases, your work might not end up @@ -75,7 +75,7 @@ In order to make a successful contribution, it is **required** that you get fami ## Automated checks The project includes various CI workflows that will run automatically for your pull request after every push and check -your changed with various tools. These tools are here to ensure that our contributing guidelines are met and ensure +your changes with various tools. These tools are here to ensure that our contributing guidelines are met and ensure good code quality of your PR. That said, you shouldn't rely on these CI workflows to let you know if you made a mistake, instead, you should run diff --git a/docs/contributing/reporting-a-bug.md b/docs/contributing/reporting-a-bug.md index 068b50b1..d2950f69 100644 --- a/docs/contributing/reporting-a-bug.md +++ b/docs/contributing/reporting-a-bug.md @@ -106,8 +106,8 @@ have 2 choices: If you don't wish to solve the bug yourself, all that remains is waiting for us to handle it. -Please understand that we are all volunteers here and we work on the project simply the fun of it. This means that we -may sometimes have other priorities in life or we just want to work on some more interesting tasks first. It might +Please understand that we are all volunteers here and we work on the project simply for the fun of it. This means that +we may sometimes have other priorities in life or we just want to work on some more interesting tasks first. It might therefore take a while for us to get to your bug (don't worry though, in most cases, we're pretty quick). Even if things are slower, we kindly ask you to avoid posting comments like "Any progress on this?" as they are not helpful and create unnecessary clutter in the discussion. From f1fe42ef78d14dff001d5c9e95d0b1a56d8e5bc2 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 8 Jan 2025 17:18:12 +0100 Subject: [PATCH 73/85] Add FAQ page --- docs/faq.md | 25 +++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 26 insertions(+) create mode 100644 docs/faq.md diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..5ada492a --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,25 @@ +--- +hide: + - navigation +--- + +# Frequently Asked Questions + +!!! bug "Work In Progress" + + This page is still being worked on, if you have any suggestions for a question, feel free to create an issue on + GitHub, or let us know on the development discord server. + +## Missing synchronous alternatives for some functions + +While mcproto does provide synchronous functionalities for the general protocol interactions (reading/writing packets +and lower level structures), any unrelated functionalities (such as HTTP interactions with the Minecraft API) will only +provide asynchronous versions. + +This was done to reduce the burden of maintaining 2 versions of the same code. The only reason protocol interaction +even have synchronous support is because it's needed for the [`Buffer`][mcproto.buffer.Buffer] +class. (See [Issue \#128](https://github.com/py-mine/mcproto/issues/128) for more details on this decision.) + +Generally, we recommend that you just stick to using the asynchronous alternatives though, both since some functions +only support async, and because async will generally provide you with a more scalable codebase, making it much easier +to handle multiple things concurrently. diff --git a/mkdocs.yml b/mkdocs.yml index 26c2c308..78e6f876 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,6 +22,7 @@ nav: - First steps: usage/first-steps.md - Packet Communication: usage/packet-communication.md - Authentication: usage/authentication.md + - FAQ: faq.md - Community: - Code of Conduct: community/code-of-conduct.md - Attributions: community/attribution.md From a600f28d89b9e285ac821f09213728e14b5d510b Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 8 Jan 2025 17:43:00 +0100 Subject: [PATCH 74/85] Add security policy --- SECURITY.md | 6 ++---- docs/contributing/security-policy.md | 3 +++ mkdocs.yml | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 docs/contributing/security-policy.md diff --git a/SECURITY.md b/SECURITY.md index 7f524663..18297e70 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,3 @@ -# Security Policy - ## Reporting Security Vulnerabilities **We urge you not to file a bug report in the GitHub issue tracker, since they are open for anyone to see** @@ -15,5 +13,5 @@ of the people below: - **ItsDrike** (project maintainer and owner) - **Email:** `itsdrike@protonmail.com` - - **Discord:** `ItsDrike#5359` (however you will need to join the [py-mine discord](https://discord.gg/C2wX7zduxC) too, - as I might not answer to message requests from people I don't share a server with.) + - **Discord:** `ItsDrike` (however you will need to join the [py-mine discord](https://discord.gg/C2wX7zduxC) too, + as I might not answer to message requests from people I don't share a server with.) diff --git a/docs/contributing/security-policy.md b/docs/contributing/security-policy.md new file mode 100644 index 00000000..61d98a3f --- /dev/null +++ b/docs/contributing/security-policy.md @@ -0,0 +1,3 @@ +# Security Policy + +--8<-- "SECURITY.md" diff --git a/mkdocs.yml b/mkdocs.yml index 78e6f876..e4011f95 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,7 @@ repo_url: https://github.com/py-mine/mcproto repo_name: py-mine/mcproto watch: - [LICENSE.txt, LICENSE-THIRD-PARTY.txt, ATTRIBUTION.md, CHANGELOG.md, changes] + [LICENSE.txt, LICENSE-THIRD-PARTY.txt, ATTRIBUTION.md, SECURITY.md, CHANGELOG.md, changes] exclude_docs: | LICENSE.md @@ -31,6 +31,7 @@ nav: - Reporting a bug: contributing/reporting-a-bug.md - Asking a question: https://github.com/py-mine/mcproto/discussions - Making a pull request: contributing/making-a-pr.md + - Security Policy: contributing/security-policy.md - Guides: - contributing/guides/index.md - Setting things up: contributing/guides/setup.md From 835b4449a7d6a93c4c9a91275f97c7138b09455f Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 2 Feb 2025 14:37:02 +0100 Subject: [PATCH 75/85] Remove private api reference docs --- docs/reference/{public => }/authentication.md | 0 docs/reference/{public => }/encryption.md | 0 docs/reference/{public => }/multiplayer.md | 0 docs/reference/{public => }/packets.md | 0 docs/reference/private/abc.md | 5 ---- docs/reference/private/authentication.md | 9 ------ docs/reference/private/deprecation.md | 3 -- docs/reference/private/index.md | 11 -------- docs/reference/private/packets.md | 18 ------------ docs/reference/private/protocol.md | 11 -------- docs/reference/private/types.md | 28 ------------------- docs/reference/{public => }/protocol.md | 0 docs/reference/{public => }/types.md | 0 mkdocs.yml | 20 ++++--------- 14 files changed, 6 insertions(+), 99 deletions(-) rename docs/reference/{public => }/authentication.md (100%) rename docs/reference/{public => }/encryption.md (100%) rename docs/reference/{public => }/multiplayer.md (100%) rename docs/reference/{public => }/packets.md (100%) delete mode 100644 docs/reference/private/abc.md delete mode 100644 docs/reference/private/authentication.md delete mode 100644 docs/reference/private/deprecation.md delete mode 100644 docs/reference/private/index.md delete mode 100644 docs/reference/private/packets.md delete mode 100644 docs/reference/private/protocol.md delete mode 100644 docs/reference/private/types.md rename docs/reference/{public => }/protocol.md (100%) rename docs/reference/{public => }/types.md (100%) diff --git a/docs/reference/public/authentication.md b/docs/reference/authentication.md similarity index 100% rename from docs/reference/public/authentication.md rename to docs/reference/authentication.md diff --git a/docs/reference/public/encryption.md b/docs/reference/encryption.md similarity index 100% rename from docs/reference/public/encryption.md rename to docs/reference/encryption.md diff --git a/docs/reference/public/multiplayer.md b/docs/reference/multiplayer.md similarity index 100% rename from docs/reference/public/multiplayer.md rename to docs/reference/multiplayer.md diff --git a/docs/reference/public/packets.md b/docs/reference/packets.md similarity index 100% rename from docs/reference/public/packets.md rename to docs/reference/packets.md diff --git a/docs/reference/private/abc.md b/docs/reference/private/abc.md deleted file mode 100644 index 47c67757..00000000 --- a/docs/reference/private/abc.md +++ /dev/null @@ -1,5 +0,0 @@ -# Utility Abstract Base Classes - -These are some internal ABC classes that we utilize in various places. - -::: mcproto.utils.abc diff --git a/docs/reference/private/authentication.md b/docs/reference/private/authentication.md deleted file mode 100644 index 38f6224f..00000000 --- a/docs/reference/private/authentication.md +++ /dev/null @@ -1,9 +0,0 @@ -# Internal components for authentication - -These are the utility components related to / used in the authentication module. - -::: mcproto.auth.msa - options: - members: [MSAAccount] - filters: - - "^_[^_]" diff --git a/docs/reference/private/deprecation.md b/docs/reference/private/deprecation.md deleted file mode 100644 index 6eaf0e9f..00000000 --- a/docs/reference/private/deprecation.md +++ /dev/null @@ -1,3 +0,0 @@ -# Deprecation utilities - -::: mcproto.utils.deprecation diff --git a/docs/reference/private/index.md b/docs/reference/private/index.md deleted file mode 100644 index 3d3bcdc2..00000000 --- a/docs/reference/private/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Private API Reference - -This is the reference documentation page for **private** mcproto API. - -!!! warning - - Private here means that the functions/classes documented here are only meant to be used internally by the library, - **you shouldn't use these in your code** if you're just a user of this library. This page is here mainly as a - reference for contributors and for providing proper linkable references. The backwards compatibility of these - components will not be guarranteed, which means breaking changes may be introduced between patch versinos without - any warnings. diff --git a/docs/reference/private/packets.md b/docs/reference/private/packets.md deleted file mode 100644 index bc8b4fdc..00000000 --- a/docs/reference/private/packets.md +++ /dev/null @@ -1,18 +0,0 @@ -# Internal components for packets - -These are the utility components related to / used in the packets module. - -!!! bug "Missing internal components of individual packets" - - The internal components which are specific to the individual packet classes (rather than being shared across the - entire packets module) are not documented here. Documentation for these may be added in the future, but there is - no timeframe for this, if you're interested in these, we recommend that you just check the source code instead. - -::: mcproto.packets.packet_map - options: - members: [WalkableModuleData, _walk_submodules, _walk_module_packets] - -::: mcproto.packets.interactions - options: - filters: - - "^_[^_]" diff --git a/docs/reference/private/protocol.md b/docs/reference/private/protocol.md deleted file mode 100644 index bfe6d87c..00000000 --- a/docs/reference/private/protocol.md +++ /dev/null @@ -1,11 +0,0 @@ -# Internal components for protocol - -These are the utility components related to / used in the protocol module. - -::: mcproto.protocol.utils - -::: mcproto.protocol.base_io - options: - members: [BaseAsyncWriter, BaseSyncWriter, BaseAsyncReader, BaseSyncReader] - filters: - - "^_[^_]" diff --git a/docs/reference/private/types.md b/docs/reference/private/types.md deleted file mode 100644 index 866f269d..00000000 --- a/docs/reference/private/types.md +++ /dev/null @@ -1,28 +0,0 @@ -# Internal components for types - -These are the utility components related to / used in the types module. - -::: mcproto.types.nbt.NBTag - options: - show_root_heading: true - show_root_toc_entry: true - filters: - - "^_[^_]" - -::: mcproto.types.nbt._NumberNBTag - options: - show_root_heading: true - show_root_toc_entry: true - members: [payload] - -::: mcproto.types.nbt._FloatingNBTag - options: - show_root_heading: true - show_root_toc_entry: true - members: [payload] - -::: mcproto.types.nbt._NumberArrayNBTag - options: - show_root_heading: true - show_root_toc_entry: true - members: [payload] diff --git a/docs/reference/public/protocol.md b/docs/reference/protocol.md similarity index 100% rename from docs/reference/public/protocol.md rename to docs/reference/protocol.md diff --git a/docs/reference/public/types.md b/docs/reference/types.md similarity index 100% rename from docs/reference/public/types.md rename to docs/reference/types.md diff --git a/mkdocs.yml b/mkdocs.yml index e4011f95..f4dedd28 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,20 +46,12 @@ nav: - Documentation: contributing/guides/documentation.md - Great commits: contributing/guides/great-commits.md - API Reference: - - Protocol: reference/public/protocol.md - - Authentication: reference/public/authentication.md - - Encryption: reference/public/encryption.md - - Multiplayer: reference/public/multiplayer.md - - Types: reference/public/types.md - - Packets: reference/public/packets.md - - Private API Reference: - - reference/private/index.md - - ABCs: reference/private/abc.md - - Deprecation: reference/private/deprecation.md - - Authentication: reference/private/authentication.md - - Protocol: reference/private/protocol.md - - Types: reference/private/types.md - - Packets: reference/private/packets.md + - Protocol: reference/protocol.md + - Authentication: reference/authentication.md + - Encryption: reference/encryption.md + - Multiplayer: reference/multiplayer.md + - Types: reference/types.md + - Packets: reference/packets.md theme: name: material From aba88ae832691f4febf40e6e610f3cba04582750 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 2 Feb 2025 16:25:35 +0100 Subject: [PATCH 76/85] Add documentation writing docs --- docs/contributing/guides/documentation.md | 24 ++++++++++++++++++++--- mkdocs.yml | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/contributing/guides/documentation.md b/docs/contributing/guides/documentation.md index 92f98b9c..8f15f420 100644 --- a/docs/contributing/guides/documentation.md +++ b/docs/contributing/guides/documentation.md @@ -1,5 +1,23 @@ -!!! bug "Work In Progress" +# Writing documentation - This page is still being written. The content below (if any) may change. +Our documentation page is generated from markdown files in the `docs/` directory, using +[`mkdocs`](https://www.mkdocs.org/) with [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material/). -# Writing documentation +This gives us an amazing framework for building great-looking, modern docs. For the most part, the documentation is +written in classical markdown syntax, just with some additions. If you're familiar with markdown, you should be able to +make a simple change easily, without having to look at any docs. + +That said, for more complex changes, you will want to familiarize yourself with [mkdocs-material +documentation](https://squidfunk.github.io/mkdocs-material/getting-started/). Don't worry, these docs are fairly easy +to read and while they do cover a lot, they're nicely segmented, so you should be able to find what you're looking for +quickly. On top of just that, you may want to simply look through the existing pages, as a lot of what you'd probably +want to do was already done on one of our pages, so you can just copy that. + +Other than just mkdocs-material, we also use +[pymdown-extensions](https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex/), which add various neat +extensions that are often useful when writing the docs. These are mostly small quality-of-life extensions that bring +some more life to the docs, but aren't something that you'd need to work with all the time. We do suggest that you check +it out though, so that you know what's available. + +Finally, for generating our API reference page, we're using [mkdocstrings](https://mkdocstrings.github.io/). More on +that in the [docstrings](./docstrings.md) guide though. diff --git a/mkdocs.yml b/mkdocs.yml index f4dedd28..2195bf84 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,7 +36,6 @@ nav: - contributing/guides/index.md - Setting things up: contributing/guides/setup.md - Style Guide: contributing/guides/style-guide.md - - Docstring formatting: contributing/guides/docstrings.md - Type hinting: contributing/guides/type-hints.md - Slotscheck: contributing/guides/slotscheck.md - Pre-commit: contributing/guides/precommit.md @@ -44,6 +43,7 @@ nav: - Breaking Changes: contributing/guides/breaking-changes.md - Unit Tests: contributing/guides/unit-tests.md - Documentation: contributing/guides/documentation.md + - Docstring formatting: contributing/guides/docstrings.md - Great commits: contributing/guides/great-commits.md - API Reference: - Protocol: reference/protocol.md From 373648432f2f75841cbbade519a712bcc505fa97 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 2 Feb 2025 17:18:15 +0100 Subject: [PATCH 77/85] Add a docstring formatting directive (google) --- docs/contributing/guides/docstrings.md | 107 +++++++++++++++++++++++++ mkdocs.yml | 2 +- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/docs/contributing/guides/docstrings.md b/docs/contributing/guides/docstrings.md index 09bb556a..ad1d27b2 100644 --- a/docs/contributing/guides/docstrings.md +++ b/docs/contributing/guides/docstrings.md @@ -3,3 +3,110 @@ This page is still being written. The content below (if any) may change. # Docstring formatting directive + +As was already briefly mentioned in the [documentation](./documentation.md) section, we're using +[mkdocstrings](https://mkdocstrings.github.io/), which is an extension of `mkdocs` that is able to automatically +generate documentation from the source code. + +Well, we're using `mkdocstrings`, but internally, the python handler for `mkdocstrings` is using +[`griffe`](https://mkdocstrings.github.io/griffe/), which is the tool responsible for actually analyzing the source +code and collecting all the details. + +As you might imagine though, in order to allow `griffe` to automatically pick up information about our codebase, it's +necessary to actually include this information into the code, as you're writing it. It's also important to use a +consistent style, that `griffe` can understand. + +In our case, we use the [Google style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for +writing docstrings. + +## Google Style docstrings + +While you should ideally just read over the [official +specification](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) (don't worry, it's actually +quite readable; well, other than the white theme) + +That said, you can also take a quick glance through some of these examples below, that quickly demonstrate the style. + +```python +def deal_damage(entity: Entity, damage: int) -> None: + """Deal damage to specified entity. + + Args: + entity: The entity to deal damage to + damage: The amount of damage to deal. + + Note: + This might end up killing the entity. If this does occur + a death message will be logged. + """ + entity.hp -= damage + if entity.hp <= 0: + print(f"Entity {entity.name} died.") + + +def bake_cookie(flavor: str, temperature: int = 175) -> str: + """Bake a delicious cookie. + + This function simulates the process of baking a cookie with the given flavor. + + Args: + flavor: The type of cookie to bake. Must be a valid flavor. + temperature: The baking temperature in Celsius. + Defaults to 175. + + Returns: + A string confirming that the cookie is ready. + + Raises: + ValueError: If the flavor is unknown. + RuntimeError: If the oven temperature is too high and the cookie burns. + """ + valid_flavors = {"chocolate chip", "oatmeal", "peanut butter", "sugar"} + if flavor not in valid_flavors: + raise ValueError(f"Unknown flavor: {flavor}") + + if temperature > 500: + raise RuntimeError("Oven overheated! Your cookie is now charcoal.") + + return f"Your {flavor} cookie is baked at {temperature}°F and ready to eat!" + + +class Cat: + """A simple representation of a cat. + + Attributes: + name: The name of the cat. + age: The age of the cat in years. + is_hungry: Whether the cat is hungry. + """ + + def __init__(self, name: str, age: int): + """Initialize a cat with a name and age. + + Args: + name: The name of the cat. + age: The age of the cat in years. + """ + self.name = name + self.age = age + self.is_hungry = True # a new cat is always hungry (duh!) + + def purr(self) -> str: + """Make the cat purr.""" + return "Purr... Purr..." + + def meow(self) -> str: + """Make the cat meow. + + Returns: + A string representing the cat's meow. + """ + return f"{self.name} says 'Meow!'" + + def feed(self) -> None: + """Feed the cat. + + Once fed, the cat will no longer be hungry. + """ + self.is_hungry = False +``` diff --git a/mkdocs.yml b/mkdocs.yml index 2195bf84..b779f2ef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -137,7 +137,7 @@ plugins: show_root_heading: false show_root_toc_entry: false show_source: false - docstring_style: sphinx + docstring_style: google relative_crossrefs: true scoped_crossrefs: true show_signature_annotations: true From ff7fa2dff42747c53eb578788589845af3ceabd6 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 5 Feb 2025 13:17:26 +0100 Subject: [PATCH 78/85] Format mkdocs.yml --- mkdocs.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index b779f2ef..6247ae1b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,14 @@ repo_url: https://github.com/py-mine/mcproto repo_name: py-mine/mcproto watch: - [LICENSE.txt, LICENSE-THIRD-PARTY.txt, ATTRIBUTION.md, SECURITY.md, CHANGELOG.md, changes] + [ + LICENSE.txt, + LICENSE-THIRD-PARTY.txt, + ATTRIBUTION.md, + SECURITY.md, + CHANGELOG.md, + changes, + ] exclude_docs: | LICENSE.md From 2916edf99e072094fcbbed23f04064ea6cbe2a92 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 5 Feb 2025 15:22:05 +0100 Subject: [PATCH 79/85] Improve docstrings guide --- ...strings.md => docstrings-and-reference.md} | 64 +++++++++++++++++-- docs/contributing/guides/documentation.md | 2 +- mkdocs.yml | 2 +- 3 files changed, 61 insertions(+), 7 deletions(-) rename docs/contributing/guides/{docstrings.md => docstrings-and-reference.md} (53%) diff --git a/docs/contributing/guides/docstrings.md b/docs/contributing/guides/docstrings-and-reference.md similarity index 53% rename from docs/contributing/guides/docstrings.md rename to docs/contributing/guides/docstrings-and-reference.md index ad1d27b2..66255c57 100644 --- a/docs/contributing/guides/docstrings.md +++ b/docs/contributing/guides/docstrings-and-reference.md @@ -2,7 +2,7 @@ This page is still being written. The content below (if any) may change. -# Docstring formatting directive +# Docstrings and API reference As was already briefly mentioned in the [documentation](./documentation.md) section, we're using [mkdocstrings](https://mkdocstrings.github.io/), which is an extension of `mkdocs` that is able to automatically @@ -19,13 +19,12 @@ consistent style, that `griffe` can understand. In our case, we use the [Google style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for writing docstrings. -## Google Style docstrings +## Google Style docstrings formatting While you should ideally just read over the [official specification](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) (don't worry, it's actually -quite readable; well, other than the white theme) - -That said, you can also take a quick glance through some of these examples below, that quickly demonstrate the style. +quite readable; well, other than the white theme), you can also take a quick glance through some of these examples +below, that quickly demonstrate the style. ```python def deal_damage(entity: Entity, damage: int) -> None: @@ -109,4 +108,59 @@ class Cat: Once fed, the cat will no longer be hungry. """ self.is_hungry = False + +DEFAULT_HP = 500 +"""This is the default value for the amount of health points that each entity will have.""" ``` + +!!! tip "Further reading" + + - Like mentioned above, you can take a look over the [official Google style guide + spec](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) + - Griffe also has a [docstring recommendations + page](https://mkdocstrings.github.io/griffe/guide/users/recommendations/docstrings/), where you can find a bunch + of examples that showcase the various places where you can use docstrings. + - On top of the general docstring recommendations, griffe also has a bit more detailed [reference + page](https://mkdocstrings.github.io/griffe/reference/docstrings/#google-style) that further demonstrates some of + the things that will and won't work. + +### Cross-References + +If you need to refer to some object (function/class/attribute/...) from you docstring, you will need to follow the +[mkdocstrings cross-references syntax](https://mkdocstrings.github.io/usage/#cross-references). Generally, it will look +something like this: + +```python title="mcproto/module_b.py" +from mcproto.module_a import custom_object + +def bar(obj): ... + +def foo(): + """Do the foo. + + This function does the foo by utilizing the [`bar`][mcproto.module_b.bar] method, + to which the [`custom_object`][mcproto.module_a.custom_object] is passed. + """ + bar(custom_object) +``` + +The references need to point to an object that is included in the docs (documented in API reference pages). **You will +need to use the fully qualified name**. (You can't just use `[bar][bar]`, even if `bar` is defined in the same scope +within the code. You will still need `[bar][mcproto.module_b.bar]`.) Sadly, while relative cross-references are +supported, [mkdocstrings gates them for sponsors +only](https://mkdocstrings.github.io/python/usage/configuration/docstrings/#relative_crossrefs), at least until a +funding goal is reached. + +## Writing API Reference + +On top of just learning about how to write docstrings, you will need to understand how to write the docs for the API +reference. Currently, most of our API reference docs work by simply recursively including the whole module, so you +likely won't need to touch it unless you're adding new modules (files). That said, sometimes, it might be useful to +document something from the docs directly, rather than just from docstrings. + +Rather than rewriting what's already really well explained, we'll instead just point you towards the [mkdocstrings +documentation](https://mkdocstrings.github.io/usage/). + +Finally, before including something into the docs, make sure it makes sense as a part of your Public API. When deciding +this, you might find this [Griffe +guide](https://mkdocstrings.github.io/griffe/guide/users/recommendations/public-apis/) to be helpful. diff --git a/docs/contributing/guides/documentation.md b/docs/contributing/guides/documentation.md index 8f15f420..1c32b82c 100644 --- a/docs/contributing/guides/documentation.md +++ b/docs/contributing/guides/documentation.md @@ -20,4 +20,4 @@ some more life to the docs, but aren't something that you'd need to work with al it out though, so that you know what's available. Finally, for generating our API reference page, we're using [mkdocstrings](https://mkdocstrings.github.io/). More on -that in the [docstrings](./docstrings.md) guide though. +that in the [docstrings and reference](./docstrings-and-reference.md) guide though. diff --git a/mkdocs.yml b/mkdocs.yml index 6247ae1b..e87a6cb5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,7 +50,7 @@ nav: - Breaking Changes: contributing/guides/breaking-changes.md - Unit Tests: contributing/guides/unit-tests.md - Documentation: contributing/guides/documentation.md - - Docstring formatting: contributing/guides/docstrings.md + - Docstrings and API Reference: contributing/guides/docstrings-and-reference.md - Great commits: contributing/guides/great-commits.md - API Reference: - Protocol: reference/protocol.md From d8a434d29271d60b6ac892365a50fc64f5ef924d Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 5 Feb 2025 15:57:31 +0100 Subject: [PATCH 80/85] Move to mkdocstrings-python-xref --- .../guides/docstrings-and-reference.md | 17 +++++++-- mkdocs.yml | 4 +- poetry.lock | 37 +++++++++++++------ pyproject.toml | 6 ++- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/docs/contributing/guides/docstrings-and-reference.md b/docs/contributing/guides/docstrings-and-reference.md index 66255c57..9cf97843 100644 --- a/docs/contributing/guides/docstrings-and-reference.md +++ b/docs/contributing/guides/docstrings-and-reference.md @@ -144,13 +144,22 @@ def foo(): bar(custom_object) ``` -The references need to point to an object that is included in the docs (documented in API reference pages). **You will -need to use the fully qualified name**. (You can't just use `[bar][bar]`, even if `bar` is defined in the same scope -within the code. You will still need `[bar][mcproto.module_b.bar]`.) Sadly, while relative cross-references are -supported, [mkdocstrings gates them for sponsors +The references need to point to an object that is included in the docs (documented in API reference pages). + +### Relative Cross-References + +While relative cross-references are supported by mkdocstrings, they are [gated for sponsors only](https://mkdocstrings.github.io/python/usage/configuration/docstrings/#relative_crossrefs), at least until a funding goal is reached. +For that reason, we're using an alternative handler to `mkdocstrings-python`: +[`mkdocstrings-python-xref`](https://github.com/analog-garage/mkdocstrings-python-xref). This handler uses +`mkdocstrings-python` internally, while extending it to provide support for relative cross-references. It is expected +that once relative cross-refs come to mainline `mkdocstrings-python`, this alternative handler will be dropped. + +To use relative cross-references, check the [mkdocstrings-python-xref +documentation](https://analog-garage.github.io/mkdocstrings-python-xref). + ## Writing API Reference On top of just learning about how to write docstrings, you will need to understand how to write the docs for the API diff --git a/mkdocs.yml b/mkdocs.yml index e87a6cb5..66739d68 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -135,9 +135,9 @@ plugins: version_selector: true - mkdocstrings: enable_inventory: true - default_handler: python + default_handler: python_xref handlers: - python: + python_xref: options: docstring_options: ignore_init_summary: true diff --git a/poetry.lock b/poetry.lock index d23d1971..65d2e942 100644 --- a/poetry.lock +++ b/poetry.lock @@ -923,23 +923,24 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.28.0" +version = "0.27.0" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.9" files = [ - {file = "mkdocstrings-0.28.0-py3-none-any.whl", hash = "sha256:84cf3dc910614781fe0fee46ce8006fde7df6cc7cca2e3f799895fb8a9170b39"}, - {file = "mkdocstrings-0.28.0.tar.gz", hash = "sha256:df20afef1eafe36ba466ae20732509ecb74237653a585f5061937e54b553b4e0"}, + {file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"}, + {file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"}, ] [package.dependencies] +click = ">=7.0" importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} Jinja2 = ">=2.11.1" Markdown = ">=3.6" MarkupSafe = ">=1.1" mkdocs = ">=1.4" -mkdocs-autorefs = ">=1.3" -mkdocs-get-deps = ">=0.2" +mkdocs-autorefs = ">=1.2" +platformdirs = ">=2.2" pymdown-extensions = ">=6.3" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} @@ -950,20 +951,34 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.14.5" +version = "1.13.0" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.9" files = [ - {file = "mkdocstrings_python-1.14.5-py3-none-any.whl", hash = "sha256:ac394f273ae298aeaa6be4506768f05e61bd7c8119437ea98553354b1185c469"}, - {file = "mkdocstrings_python-1.14.5.tar.gz", hash = "sha256:8582eeac8cce952f395d76ec636fc814757cba7d8458aa75ba0529a3aa10d98c"}, + {file = "mkdocstrings_python-1.13.0-py3-none-any.whl", hash = "sha256:b88bbb207bab4086434743849f8e796788b373bd32e7bfefbf8560ac45d88f97"}, + {file = "mkdocstrings_python-1.13.0.tar.gz", hash = "sha256:2dbd5757e8375b9720e81db16f52f1856bf59905428fd7ef88005d1370e2f64c"}, ] [package.dependencies] griffe = ">=0.49" mkdocs-autorefs = ">=1.2" -mkdocstrings = ">=0.28" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} +mkdocstrings = ">=0.26" + +[[package]] +name = "mkdocstrings-python-xref" +version = "1.6.2" +description = "Enhanced mkdocstrings python handler" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python_xref-1.6.2-py3-none-any.whl", hash = "sha256:1492b822bcb04a9072a9b731ceabfed4b9d3f8cd23c62211cb8b22bab0cb7fc0"}, + {file = "mkdocstrings_python_xref-1.6.2.tar.gz", hash = "sha256:65e83fecc3a059d173aab9b06e4bcd50cc720b221b3cc3d6b35b11ffb6aa86d4"}, +] + +[package.dependencies] +griffe = ">=1.0" +mkdocstrings-python = ">=1.6.2,<2.0" [[package]] name = "mslex" @@ -1789,4 +1804,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "d58080b11e4e08a15244e102d722aa30a8a72292c03fff8e09da64b3a9436642" +content-hash = "a203277c759fae13ae4e57b94819110b14d9eb3570faf45ac51be3662bdf1fca" diff --git a/pyproject.toml b/pyproject.toml index 31f1ec3c..f4afac18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,8 +67,10 @@ mkdocs = "^1.6.0" mkdocs-material = "^9.5.30" mike = "^2.1.2" markdown-exec = { extras = ["ansi"], version = "^1.9.3" } -towncrier = ">=23,<24.7" # temporary pin, as 24.7 is incompatible with sphinxcontrib-towncrier -mkdocstrings-python = { version = "^1.12.1", python = ">3.9,<4" } +towncrier = ">=23,<24.7" # temporary pin, as 24.7 is incompatible with sphinxcontrib-towncrier +mkdocstrings = "0.27.0" +mkdocstrings-python = "1.13.0" +mkdocstrings-python-xref = "^1.6.2" [tool.poetry.group.docs-ci] optional = true From 36bef4010a638985eea4b2bd0e16467aa1fc3e18 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 5 Feb 2025 22:32:16 +0100 Subject: [PATCH 81/85] Add missing abstract for documentation.md --- docs/contributing/guides/documentation.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/contributing/guides/documentation.md b/docs/contributing/guides/documentation.md index 1c32b82c..8faf747f 100644 --- a/docs/contributing/guides/documentation.md +++ b/docs/contributing/guides/documentation.md @@ -1,5 +1,10 @@ # Writing documentation +???+ abstract + + This guide describes how to write the documentation for this project (like the docs for the page you're reading + right now). It contains several useful links for `mkdocs` documentation and for the various extensions that we use. + Our documentation page is generated from markdown files in the `docs/` directory, using [`mkdocs`](https://www.mkdocs.org/) with [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material/). From 5689952f5443268a34d3449eef44a5da59a2111f Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 5 Feb 2025 22:33:47 +0100 Subject: [PATCH 82/85] Update the api reference docs --- .../{docstrings-and-reference.md => api-reference.md} | 11 ++++++++++- docs/contributing/guides/documentation.md | 2 +- mkdocs.yml | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) rename docs/contributing/guides/{docstrings-and-reference.md => api-reference.md} (94%) diff --git a/docs/contributing/guides/docstrings-and-reference.md b/docs/contributing/guides/api-reference.md similarity index 94% rename from docs/contributing/guides/docstrings-and-reference.md rename to docs/contributing/guides/api-reference.md index 9cf97843..01fc052a 100644 --- a/docs/contributing/guides/docstrings-and-reference.md +++ b/docs/contributing/guides/api-reference.md @@ -2,7 +2,14 @@ This page is still being written. The content below (if any) may change. -# Docstrings and API reference +# API reference + +???+ abstract + +This page contains the guide on documenting the code that will appear in the API reference section of this +documentation. It goes over the technology and libraries that we use to generate this API reference docs, details the +docstring style we use, mentions how to add something into the API reference (like new modules) and details what +should and shouldn't be documented here. As was already briefly mentioned in the [documentation](./documentation.md) section, we're using [mkdocstrings](https://mkdocstrings.github.io/), which is an extension of `mkdocs` that is able to automatically @@ -170,6 +177,8 @@ document something from the docs directly, rather than just from docstrings. Rather than rewriting what's already really well explained, we'll instead just point you towards the [mkdocstrings documentation](https://mkdocstrings.github.io/usage/). +## What to document + Finally, before including something into the docs, make sure it makes sense as a part of your Public API. When deciding this, you might find this [Griffe guide](https://mkdocstrings.github.io/griffe/guide/users/recommendations/public-apis/) to be helpful. diff --git a/docs/contributing/guides/documentation.md b/docs/contributing/guides/documentation.md index 8faf747f..5f5cec17 100644 --- a/docs/contributing/guides/documentation.md +++ b/docs/contributing/guides/documentation.md @@ -25,4 +25,4 @@ some more life to the docs, but aren't something that you'd need to work with al it out though, so that you know what's available. Finally, for generating our API reference page, we're using [mkdocstrings](https://mkdocstrings.github.io/). More on -that in the [docstrings and reference](./docstrings-and-reference.md) guide though. +that in the [API reference](./api-reference.md) guide though. diff --git a/mkdocs.yml b/mkdocs.yml index 66739d68..e3622c8a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,7 +50,7 @@ nav: - Breaking Changes: contributing/guides/breaking-changes.md - Unit Tests: contributing/guides/unit-tests.md - Documentation: contributing/guides/documentation.md - - Docstrings and API Reference: contributing/guides/docstrings-and-reference.md + - API Reference: contributing/guides/api-reference.md - Great commits: contributing/guides/great-commits.md - API Reference: - Protocol: reference/protocol.md From 6c2bf176d518a428fd35391e9b584aba0c1c39aa Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 5 Feb 2025 22:37:20 +0100 Subject: [PATCH 83/85] Fix abstract in api reference --- docs/contributing/guides/api-reference.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/contributing/guides/api-reference.md b/docs/contributing/guides/api-reference.md index 01fc052a..364252e7 100644 --- a/docs/contributing/guides/api-reference.md +++ b/docs/contributing/guides/api-reference.md @@ -6,10 +6,10 @@ ???+ abstract -This page contains the guide on documenting the code that will appear in the API reference section of this -documentation. It goes over the technology and libraries that we use to generate this API reference docs, details the -docstring style we use, mentions how to add something into the API reference (like new modules) and details what -should and shouldn't be documented here. + This page contains the guide on documenting the code that will appear in the API reference section of this + documentation. It goes over the technology and libraries that we use to generate this API reference docs, details + the docstring style we use, mentions how to add something into the API reference (like new modules) and details what + should and shouldn't be documented here. As was already briefly mentioned in the [documentation](./documentation.md) section, we're using [mkdocstrings](https://mkdocstrings.github.io/), which is an extension of `mkdocs` that is able to automatically From c85c6ec44872d6930df18111247e7865c4a5ed06 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Wed, 5 Feb 2025 23:46:25 +0100 Subject: [PATCH 84/85] Improve wording of to code-of-conduct --- docs/community/code-of-conduct.md | 102 ++++++++++++++++-------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/docs/community/code-of-conduct.md b/docs/community/code-of-conduct.md index c0f34cd0..0589f444 100644 --- a/docs/community/code-of-conduct.md +++ b/docs/community/code-of-conduct.md @@ -1,16 +1,15 @@ # Code of Conduct -This code of conduct outlines our expectations for the people involved with this project. We, as members, contributors -and leaders are committed to providing a welcoming and inspiring project that anyone can easily join, expecting -a harassment-free experience, as described in this code of conduct. +This code of conduct outlines our expectations for the people involved with this project. We, as members, contributors, +and leaders, are committed to fostering a welcoming and inspiring project where anyone can participate with the +expectation of a harassment-free experience, as outlined in this code of conduct. -This code of conduct is here to ensure we provide a welcoming and inspiring project that anyone can easily join, -expecting a harassment-free experience, as described in this code of conduct. +The goal of this document is to set the overall tone for our community. It is here to outline some of the things you can +and can't do if you wish to participate in our community. -The goal of this document is to set the overall tone for our community. It is here to outline some of the things you -can and can't do if you wish to participate in our community. However it is not here to serve as a rule-book with -a complete set of things you can't do, social conduct differs from situation to situation, and person to person, but we -should do our best to try and provide a good experience to everyone, in every situation. +However, it is not intended as a rulebook containing an exhaustive list of permitted and prohibited actions. Social +conduct varies between situations and individuals, but we should all do our best to create a welcoming and positive +experience for everyone. We value many things beyond just technical expertise, including collaboration and supporting others within our community. Providing a positive experience for others can have a much more significant impact than simply providing the @@ -22,8 +21,8 @@ We share a common understanding of what constitutes harassment as it applies to list cannot be exhaustive, we explicitly honor the following "protected attributes": **diversity in age, gender, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, disability and personal appearance**. We will not tolerate discrimination based on any of the -protected characteristics above, including some that may not have been explicitly mentioned here. We consider -discrimination of any kind to be unacceptable and immoral. +protected characteristics above, including others not explicitly listed here. We consider discrimination of any kind to +be unacceptable and immoral. Harassment includes, but is not limited to: @@ -32,43 +31,44 @@ Harassment includes, but is not limited to: their explicit consent, except as necessary means to protect others from intentional abuse. - Unwelcome comments regarding a person's lifestyle choices and practices, including those related to food, health, parenting, drugs and employment. -- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a - person's gender identity. You must address people by the name they give you when not addressing them by their - username or handle. -- Threats of violence, both physical and psychological. +- Deliberate misgendering, including deadnaming or persistently using a pronoun that does not correctly reflect a + person's gender identity. You should do your best to address people by the name/pronoun they give you when not + addressing them by their username or handle. +- Threats of physical or psychological violence. - Incitement of violence towards any individual, including encouraging a person to engage in self-harm. -- Publication of non-harassing private communication. -- Pattern of inappropriate social conduct, such as requesting/assuming inappropriate levels of intimacy with others, or - excessive teasing after a request to stop. +- Publishing private communication without consent, even if non-harassing. +- A pattern of inappropriate behavior, such as unwelcome intimacy or persistent teasing after a request to stop. - Continued one-on-one communication after requests to cease. - Sabotage of someone else's work or intentionally hindering someone else's performance. ## Plagiarism -Plagiarism is the re-use of someone else's work (eg: binary content such as images, textual content such as an article, -but also source code, or any other copyrightable resources) without the permission or a license right from the author. -Claiming someone else's work as your own is not just immoral and disrespectful to the author, but also illegal in most -countries. You should always follow the authors wishes, and give credit where credit is due. +Plagiarism is the re-use of someone else's work (e.g., binary content such as images, textual content such as an +article, but also source code, or any other copyrightable resources) without the permission or a license right from the +author. Claiming someone else's work as your own is not only unethical and disrespectful to the author, but also +illegal in most countries. You should always respect the author's wishes, and give credit where credit is due. -If we found that you've **intentionally** attempted to add plagiarized content to our code-base, you will likely end up -being permanently banned from any future contributions to this project's repository. We will of course also do our best -to remove, or properly attribute this plagiarized content as quickly as possible. +### Intentional vs. Unintentional Plagiarism -An unintentional attempt at plagiarism will not be punished as harshly, but nevertheless, it is your responsibility as -a contributor to check where the code you're submitting comes from, and so repeated submission of such content, even -after you were warned might still get you banned. +If we find that you've **intentionally** attempted to add plagiarized content to our code-base, you will likely face a +**permanent ban** from any future contributions to this project's repository. We will, of course, do our best to +remove, or properly attribute this plagiarized content as quickly as possible. -Please note that an online repository that has no license is presumed to only be source-available, NOT open-source. -Meaning that this work is protected by author's copyright, automatically imposed over it, and without any license -extending that copyright, you have no rights to use such code. So know that you can't simply take some source-code, -even though it's published publicly. This code may be available to be seen by anyone, but that does not mean it's also -available to be used by anyone in other projects. +Unintentional plagiarism will not be punished as harshly, but nevertheless, it is your responsibility as a contributor +to check where the code you're submitting comes from, and so, repeated submissions of such content, even after warnings, may still result in a ban. -Another important note to keep in mind is that even if some project has an open-source license, that license may have -conditions which are incompatible with our codebase (such as requiring all of the code that links to this new part to -also be licensed under the same license, which our code-base is not currently under). That is why it's necessary to -understand a license before using code available under it. Simple attribution often isn't everything that the license -requires. +### Understanding code licensing + +Please note that an online repository **without a license** is presumed to only be source-available, **NOT +open-source**. This means the work is **still protected by author's copyright**, automatically imposed over it and +without any license extending that copyright, you have no legal rights to use such code. **Simply finding publicly +posted code does not grant permission to reuse it in other projects.** This code may be available to be seen by anyone, +but that does not mean it's also available to be used by anyone in any way they like. + +Another important note to keep in mind is that **even if a project has an open-source license**, that license may have +conditions which are **incompatible** with our codebase. For example, some licenses require that all linked code be +licensed under the same terms, which may not align with our project's licensing. Always review and understand a license +before using code under it — **simple attribution often isn't enough**. ??? tip "Learn more about software licensing" @@ -78,14 +78,14 @@ requires. ## Generally inappropriate behavior Outside of just harassment and plagiarism, there are countless other behaviors which we consider unacceptable, as they -may be offensive, and discourage people from engaging with our community. +may be offensive or discourage people from engaging with our community. **Examples of generally inappropriate behavior:** - The use of sexualized language or imagery of any kind - The use of inappropriate images, including in an account's avatar - The use of inappropriate language, including in an account's nickname -- Any spamming, flamming, baiting or other attention-stealing behavior +- Any form of spamming, flaming, baiting or other attention-stealing / disruptive behavior that derails discussions - Discussing topics that are overly polarizing, sensitive, or incite arguments. - Responding with "RTFM", "just google it" or similar response to help requests - Other conduct which could be reasonably considered inappropriate @@ -106,9 +106,9 @@ applies when an individual is officially representing the community in public sp community include using an official social media account, or acting as an appointed representative at an online or offline event. -All members involved with the project are expected to follow this Code of Conduct, no matter their position in the -project's hierarchy, this Code of Conduct applies equally to contributors, maintainers and people seeking -help/reporting bugs, etc. +All members involved with the project are expected to follow this Code of Conduct, regardless of their position in the +project's hierarchy, this Code of Conduct applies equally to contributors, maintainers, and those seeking help or +reporting bugs. ## Enforcement Responsibilities @@ -120,18 +120,22 @@ Community leaders are responsible for clarifying and enforcing our standards of appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, harmful, or otherwise undesirable. -Community leaders have the right and responsibility to remove, edit or reject comments, commits, code, wiki edits, -issues and other contributions within the enforcement scope that are not aligned to this Code of Conduct, and will -communicate reasons for moderation decisions when appropriate. +Community leaders have the authority and responsibility to remove, edit, or reject any contributions — such as comments, +commits, code, wiki edits, issues, or Discord messages — that violate this Code of Conduct. When appropriate, they will +also make sure to communicate the reasons for moderation decisions. If you have experienced or witnessed unacceptable behavior constituting a code of conduct violation or have any other code of conduct concerns, please let us know and we will do our best to resolve this issue. ## Reporting a Code of Conduct violation -If you think that someone is violating the Code of Conduct, you can report it to any repository maintainer, either by -email or through a Discord DM. You should avoid using public channels for reporting these violations, and instead do so -in private discussion with a maintainer. +If you think that someone is violating the Code of Conduct, you can report it to any repository maintainer. When doing +so, follow these steps: + +1. Contact a repository maintainer via email or Discord DM. Avoid using public channels for these reports. +2. When submitting the report, make sure to provide all the necessary details of the incident, including context and + relevant links/screenshots. +3. We also kindly ask that you maintain confidentiality and avoid any public discussions of the violation. ## Sources From 74819ae914cbea115ac93dee4a0cb4b0aa8a4130 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Thu, 6 Feb 2025 08:41:30 +0100 Subject: [PATCH 85/85] Downgrade mkdocs-autorefs This downgrade is necessary to get rid of a deprecation warning produced by mkdocstrings-python-xref. For more info, see: https://github.com/analog-garage/mkdocstrings-python-xref/issues/29 --- poetry.lock | 10 +++++----- pyproject.toml | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 65d2e942..1e755f46 100644 --- a/poetry.lock +++ b/poetry.lock @@ -850,13 +850,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp [[package]] name = "mkdocs-autorefs" -version = "1.3.0" +version = "1.2.0" description = "Automatically link across pages in MkDocs." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" files = [ - {file = "mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3"}, - {file = "mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61"}, + {file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"}, + {file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"}, ] [package.dependencies] @@ -1804,4 +1804,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "a203277c759fae13ae4e57b94819110b14d9eb3570faf45ac51be3662bdf1fca" +content-hash = "e19c56afdb7b82c8ed527178dfb42409a8e1f7c76eb81ad5f2224c2d724ede94" diff --git a/pyproject.toml b/pyproject.toml index f4afac18..60a24b16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,8 +68,9 @@ mkdocs-material = "^9.5.30" mike = "^2.1.2" markdown-exec = { extras = ["ansi"], version = "^1.9.3" } towncrier = ">=23,<24.7" # temporary pin, as 24.7 is incompatible with sphinxcontrib-towncrier -mkdocstrings = "0.27.0" -mkdocstrings-python = "1.13.0" +mkdocs-autorefs = "1.2.0" # temporary pin for https://github.com/analog-garage/mkdocstrings-python-xref/issues/29 +mkdocstrings = "0.27.0" # temporary pin for https://github.com/analog-garage/mkdocstrings-python-xref/issues/29 +mkdocstrings-python = "1.13.0" # temporary pin for https://github.com/analog-garage/mkdocstrings-python-xref/issues/29 mkdocstrings-python-xref = "^1.6.2" [tool.poetry.group.docs-ci]