diff --git a/docs/conf.py b/docs/conf.py index dd9f7559c5..4c7bf08520 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,7 @@ extensions = [ "sphinx.ext.githubpages", "sphinx.ext.intersphinx", + "sphinx_copybutton", "myst_parser", "autodoc2", ] @@ -33,12 +34,11 @@ "autodoc2.dup_item", ] nitpick_ignore = [ - ("py:class", "puyapy._T"), - ("py:class", "puyapy._P"), - ("py:class", "puyapy._R"), - ("py:class", "puyapy.arc4._T"), - ("py:class", "puyapy.arc4._TArrayItem"), - ("py:class", "puyapy.arc4._TTuple"), + ("py:class", "puyapy.arc4.AllowedOnCompletes"), +] +nitpick_ignore_regex = [ + ("py:class", r"puyapy\._.*"), + ("py:class", r"puyapy\.arc4\._.*"), ] # -- Options for HTML output ------------------------------------------------- @@ -68,9 +68,10 @@ autodoc2_hidden_objects = [ "private", # single-underscore methods, e.g. _private "undoc", + "inherited", ] autodoc2_class_inheritance = False -autodoc2_module_all_regexes = [r"puyapy"] +autodoc2_module_all_regexes = [r"puyapy.*"] autodoc2_render_plugin = "myst" autodoc2_sort_names = True autodoc2_index_template = None diff --git a/poetry.lock b/poetry.lock index 72df0fd088..79e2bda8e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -803,13 +803,13 @@ files = [ [[package]] name = "dunamai" -version = "1.19.0" +version = "1.19.1" description = "Dynamic version generation" optional = false -python-versions = ">=3.5,<4.0" +python-versions = ">=3.5" files = [ - {file = "dunamai-1.19.0-py3-none-any.whl", hash = "sha256:1ed948676bbf0812bfaafe315a134634f8d6eb67138513c75aa66e747404b9c6"}, - {file = "dunamai-1.19.0.tar.gz", hash = "sha256:6ad99ae34f7cd290550a2ef1305d2e0292e6e6b5b1b830dfc07ceb7fd35fec09"}, + {file = "dunamai-1.19.1-py3-none-any.whl", hash = "sha256:a6aa0ae3bdb01a12d6f219555d6e230ec02663afb43a7bd37933e6c4fecefc9b"}, + {file = "dunamai-1.19.1.tar.gz", hash = "sha256:c8112efec15cd1a8e0384d5d6f3be97a01bf7bc45b54069533856aaaa33a9b9e"}, ] [package.dependencies] @@ -1120,21 +1120,21 @@ files = [ [[package]] name = "jaraco-classes" -version = "3.3.0" +version = "3.3.1" description = "Utility functions for Python class constructs" optional = false python-versions = ">=3.8" files = [ - {file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"}, - {file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"}, + {file = "jaraco.classes-3.3.1-py3-none-any.whl", hash = "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206"}, + {file = "jaraco.classes-3.3.1.tar.gz", hash = "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30"}, ] [package.dependencies] more-itertools = "*" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "jeepney" @@ -2684,6 +2684,24 @@ 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.8" @@ -3075,4 +3093,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "391c42e79502253287b9c95cfa41106d6a2a836af5da578c392f2ed9eb5ff018" +content-hash = "1ca8d42e072055c64ef3b7b0db42b847fdeffe8f709264bb6121f9a625172619" diff --git a/pyproject.toml b/pyproject.toml index 9f7dba963e..772ae9e6e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ sphinx = "^7.2.6" furo = "^2024.1.29" myst-parser = "^2.0.0" sphinx-autodoc2 = "^0.5.0" +sphinx-copybutton = "^0.5.2" [tool.poetry.group.cicd] optional = true diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py index 966eecf63b..05a5a2a378 100755 --- a/scripts/generate_docs.py +++ b/scripts/generate_docs.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import shutil import subprocess import sys from collections.abc import Callable @@ -13,7 +12,7 @@ import mypy.nodes from mypy.visitor import NodeVisitor from puya.compile import get_mypy_options -from puya.parse import parse_and_typecheck +from puya.parse import ParseResult, parse_and_typecheck SCRIPTS_DIR = Path(__file__).parent VCS_ROOT = SCRIPTS_DIR.parent @@ -31,37 +30,44 @@ class ModuleImports: def main() -> None: - stubs = combine_stubs(STUBS_DIR / "__init__.pyi") - output_combined_stub(stubs, STUBS_DOC_DIR / "__init__.pyi") + parse_result = parse_and_typecheck([STUBS_DIR], get_mypy_options()) + output_doc_stubs(parse_result) run_sphinx() -def combine_stubs(path: Path) -> "StubVisitor": - mypy_options = get_mypy_options() - parse_result = parse_and_typecheck([path], mypy_options) +def output_doc_stubs(parse_result: ParseResult) -> None: + # parse and output reformatted __init__.pyi + puyapy_stubs = parse_stubs(parse_result, "puyapy") + puyapy_direct_imports = puyapy_stubs.collected_imports["puyapy"] + # remove any puyapy imports that are now defined in __init__.py itself + for name in list(puyapy_direct_imports.from_imports): + if name in puyapy_stubs.collected_symbols: + del puyapy_direct_imports.from_imports[name] + output_combined_stub(puyapy_stubs, STUBS_DOC_DIR / "__init__.pyi") + + # remaining imports from puyapy are other public modules + # parse and output them too + for other_stub_name in puyapy_direct_imports.from_imports: + other_stubs = parse_stubs(parse_result, f"puyapy.{other_stub_name}") + output_combined_stub(other_stubs, STUBS_DOC_DIR / f"{other_stub_name}.pyi") + + +def parse_stubs(parse_result: ParseResult, module_id: str) -> "StubVisitor": read_source = parse_result.manager.errors.read_source assert read_source modules = parse_result.manager.modules - puyapy_module: mypy.nodes.MypyFile = modules["puyapy"] - stub_visitor = StubVisitor(read_source=read_source, file=puyapy_module, modules=modules) - puyapy_module.accept(stub_visitor) + module: mypy.nodes.MypyFile = modules[module_id] + stub_visitor = StubVisitor(read_source=read_source, file=module, modules=modules) + module.accept(stub_visitor) return stub_visitor def output_combined_stub(stubs: "StubVisitor", output: Path) -> None: # remove puyapy imports that have been inlined - puyapy_direct_imports = stubs.collected_imports.get("puyapy", ModuleImports()) - other_puyapy_stubs = [] - for name in list(puyapy_direct_imports.from_imports): - if name in stubs.collected_symbols or name in stubs.collected_assignments: - del puyapy_direct_imports.from_imports[name] - else: - other_puyapy_stubs.append(name) - lines = ["# ruff: noqa: A001, E501, F403, PYI021, PYI034"] rexported = list[str]() for module, imports in stubs.collected_imports.items(): - if imports.import_all: + if imports.import_module: lines.append(f"import {module}") if imports.from_imports: rexported.extend(filter(None, imports.from_imports.values())) @@ -71,26 +77,20 @@ def output_combined_stub(stubs: "StubVisitor", output: Path) -> None: # assemble __all__ lines.append("__all__ = [") - for symbol in (*rexported, *stubs.collected_assignments, *stubs.collected_symbols): + for symbol in (*rexported, *stubs.collected_symbols): if symbol.startswith("_"): continue lines.append(f' "{symbol}",') lines.append("]") - # assemble assignments - for symbol in stubs.collected_assignments.values(): - lines.append(symbol) - # assemble symbols for symbol in stubs.collected_symbols.values(): - lines.extend(["", "", symbol]) + lines.append(symbol) # output and linting output.parent.mkdir(parents=True, exist_ok=True) output.write_text("\n".join(lines)) - for other_stub in other_puyapy_stubs: - stub_file = f"{other_stub}.pyi" - shutil.copy2(STUBS_DIR / stub_file, STUBS_DOC_DIR / stub_file) + subprocess.run(["black", str(output)], check=True, cwd=VCS_ROOT) subprocess.run(["ruff", "--fix", str(output)], check=True, cwd=VCS_ROOT) @@ -104,21 +104,43 @@ def run_sphinx() -> None: @attrs.define class SymbolCollector(NodeVisitor[None]): file: mypy.nodes.MypyFile - symbols: dict[str, mypy.nodes.Node] = attrs.field(factory=dict) - assignments: dict[str, mypy.nodes.Node] = attrs.field(factory=dict) + read_source: Callable[[str], list[str] | None] + symbols: dict[str, str] = attrs.field(factory=dict) + + def get_src(self, node: mypy.nodes.Node) -> str: + return self.get_src_from_lines(node.line, node.end_line or node.line) + + def get_src_from_lines(self, line: int, end_line: int) -> str: + src = self.read_source(self.file.path) + if not src: + raise Exception("Could not get src") + return "\n".join(src[line - 1 : end_line]) def visit_mypy_file(self, o: mypy.nodes.MypyFile) -> None: for stmt in o.defs: stmt.accept(self) def visit_class_def(self, o: mypy.nodes.ClassDef) -> None: - self.symbols[o.name] = o + self.symbols[o.name] = self.get_src(o) def visit_func_def(self, o: mypy.nodes.FuncDef) -> None: - self.symbols[o.name] = o + self.symbols[o.name] = self.get_src(o) def visit_overloaded_func_def(self, o: mypy.nodes.OverloadedFuncDef) -> None: - self.symbols[o.name] = o + line = o.line + end_line = o.end_line or o.line + for item in o.items: + end_line = max(end_line, item.end_line or item.line) + overloaded_src = self.get_src_from_lines(line, end_line) + best_sig = _get_documented_overload(o) + + if not best_sig: + src = overloaded_src + else: + best_sig_src = self.get_src(best_sig) + src = f"{overloaded_src}\n{best_sig_src}" + + self.symbols[o.name] = src def visit_assignment_stmt(self, o: mypy.nodes.AssignmentStmt) -> None: try: @@ -127,7 +149,46 @@ def visit_assignment_stmt(self, o: mypy.nodes.AssignmentStmt) -> None: raise Exception(f"Multi assignments are not supported: {o}") from ex if not isinstance(lvalue, mypy.nodes.NameExpr): raise Exception(f"Multi assignments are not supported: {lvalue}") - self.assignments[lvalue.name] = o.rvalue + self.symbols[lvalue.name] = self.get_src(o.rvalue) + + +def _get_documented_overload(o: mypy.nodes.OverloadedFuncDef) -> mypy.nodes.FuncDef | None: + best_overload: mypy.nodes.FuncDef | None = None + for overload in o.items: + match overload: + case mypy.nodes.Decorator(func=func_def): + pass + case mypy.nodes.FuncDef() as func_def: + pass + case _: + raise Exception("Only function overloads supported") + + docstring = _get_docstring(func_def) + + # this is good enough until a more complex case arises + if docstring and ( + not best_overload or len(func_def.arguments) > len(best_overload.arguments) + ): + best_overload = func_def + return best_overload + + +def _get_docstring(func_def: mypy.nodes.FuncDef) -> str | None: + match func_def: + case mypy.nodes.FuncDef(docstring=str() as docstring): + return docstring + case mypy.nodes.FuncDef( + body=mypy.nodes.Block( + body=[ + mypy.nodes.ExpressionStmt( + expr=mypy.nodes.StrExpr(value=docstring), + ) + ] + ) + ): + return docstring + case _: + return None @attrs.define @@ -156,7 +217,7 @@ def visit_import(self, o: mypy.nodes.Import) -> None: raise Exception("Aliasing symbols in stubs is not supported") imports = self.get_imports(name) - imports.import_all = True + imports.import_module = True @attrs.define @@ -166,7 +227,6 @@ class StubVisitor(NodeVisitor[None]): modules: dict[str, mypy.nodes.MypyFile] parsed_modules: dict[str, SymbolCollector] = attrs.field(factory=dict) collected_imports: dict[str, ModuleImports] = attrs.field(factory=dict) - collected_assignments: dict[str, str] = attrs.field(factory=dict) collected_symbols: dict[str, str] = attrs.field(factory=dict) def _get_module(self, module_id: str) -> SymbolCollector: @@ -174,7 +234,9 @@ def _get_module(self, module_id: str) -> SymbolCollector: return self.parsed_modules[module_id] except KeyError: file = self.modules[module_id] - self.parsed_modules[module_id] = collector = SymbolCollector(file=file) + self.parsed_modules[module_id] = collector = SymbolCollector( + file=file, read_source=self.read_source + ) file.accept(collector) self._collect_imports(file) return collector @@ -185,6 +247,7 @@ def _collect_imports(self, o: mypy.nodes.Node) -> None: def visit_mypy_file(self, o: mypy.nodes.MypyFile) -> None: for stmt in o.defs: stmt.accept(self) + self._add_all_symbols(o.fullname) def visit_import_from(self, o: mypy.nodes.ImportFrom) -> None: if not _should_inline_module(o.id): @@ -195,36 +258,28 @@ def visit_import_from(self, o: mypy.nodes.ImportFrom) -> None: if name != (name_as or name): raise Exception("Aliasing symbols in stubs is not supported") - self.add_symbol(module, name, is_symbol=name in module.symbols) + self.add_symbol(module, name) def visit_import_all(self, o: mypy.nodes.ImportAll) -> None: if not _should_inline_module(o.id): self._collect_imports(o) - return - module = self._get_module(o.id) + else: + self._add_all_symbols(o.id) + + def _add_all_symbols(self, module_id: str) -> None: + module = self._get_module(module_id) for sym in module.symbols: self.add_symbol(module, sym) - for ass in module.assignments: - self.add_symbol(module, ass, is_symbol=False) def visit_import(self, o: mypy.nodes.Import) -> None: self._collect_imports(o) - def add_symbol(self, module: SymbolCollector, name: str, *, is_symbol: bool = True) -> None: - src = self.read_source(module.file.path) - if not src: - raise Exception("Could not get src") - node = module.symbols[name] if is_symbol else module.assignments[name] - lines = "\n".join(src[node.line - 1 : node.end_line]) - existing = ( - self.collected_symbols.get(name) if is_symbol else self.collected_assignments.get(name) - ) + def add_symbol(self, module: SymbolCollector, name: str) -> None: + lines = module.symbols[name] + existing = self.collected_symbols.get(name) if existing is not None and existing != lines: raise Exception(f"Duplicate definitions are not supported: {name}\n{lines}") - if is_symbol: - self.collected_symbols[name] = lines - else: - self.collected_assignments[name] = lines + self.collected_symbols[name] = lines def _name_as(name: str, name_as: str | None) -> str: diff --git a/src/puyapy-stubs/__init__.pyi b/src/puyapy-stubs/__init__.pyi index 08a8607ac8..73a901261a 100644 --- a/src/puyapy-stubs/__init__.pyi +++ b/src/puyapy-stubs/__init__.pyi @@ -1,5 +1,7 @@ # ruff: noqa: F403 # note: arc4 deliberately imported as module instead of re-exporting +# this order is intentional, so that when the stubs are processed for documentation the +# types are in the correct dependency order from puyapy import arc4 as arc4 from puyapy._primitives import * from puyapy._constants import * diff --git a/src/puyapy-stubs/_reference.pyi b/src/puyapy-stubs/_reference.pyi index 82f7b13b64..f29a8f255d 100644 --- a/src/puyapy-stubs/_reference.pyi +++ b/src/puyapy-stubs/_reference.pyi @@ -11,14 +11,16 @@ class Account: __match_value__: str __match_args__ = ("__match_value__",) def __init__(self, address: typing.LiteralString): + """`address` should be a 58 character base32 string, + ie a base32 string-encoded 32 bytes public key + 4 bytes checksum """ - `value` should be a 58 character base32 string, - ie a base32 string-encoded 32 bytes public key + 4 bytes checksum - """ - def __eq__(self, other: Account | typing.LiteralString) -> bool: ... # type: ignore[override] - def __ne__(self, other: Account | typing.LiteralString) -> bool: ... # type: ignore[override] + def __eq__(self, other: Account | typing.LiteralString) -> bool: # type: ignore[override] + """Account equality is determined by the address of another `Account` or `str`""" + def __ne__(self, other: Account | typing.LiteralString) -> bool: # type: ignore[override] + """Account equality is determined by the address of another `Account` or `str`""" # truthiness - def __bool__(self) -> bool: ... # returns True iff not equal to the zero address + def __bool__(self) -> bool: + """Returns `True` if not equal to the zero address""" @property def bytes(self) -> Bytes: """Get the byte[] backing this value. Note that Bytes is immutable""" @@ -27,134 +29,311 @@ class Account: """no validation happens here wrt length""" @property def balance(self) -> UInt64: - """Account balance in microalgos""" + """Account balance in microalgos + + ```{note} + Account must be an available resource + ``` + """ @property def min_balance(self) -> UInt64: - """Minimum required balance for account, in microalgos""" + """Minimum required balance for account, in microalgos + + ```{note} + Account must be an available resource + ``` + """ @property def auth_address(self) -> Account: - """Address the account is rekeyed to""" + """Address the account is rekeyed to + + ```{note} + Account must be an available resource + ``` + """ @property def total_num_uint(self) -> UInt64: - """The total number of uint64 values allocated by this account in Global and Local States.""" + """The total number of uint64 values allocated by this account in Global and Local States. + + ```{note} + Account must be an available resource + ``` + """ @property def total_num_byte_slice(self) -> Bytes: - """The total number of byte array values allocated by this account in Global and Local States.""" + """The total number of byte array values allocated by this account in Global and Local States. + + ```{note} + Account must be an available resource + ``` + """ @property def total_extra_app_pages(self) -> UInt64: - """The number of extra app code pages used by this account.""" + """The number of extra app code pages used by this account. + + ```{note} + Account must be an available resource + ``` + """ @property def total_apps_created(self) -> UInt64: - """The number of existing apps created by this account.""" + """The number of existing apps created by this account. + + ```{note} + Account must be an available resource + ``` + """ @property def total_apps_opted_in(self) -> UInt64: - """The number of apps this account is opted into.""" + """The number of apps this account is opted into. + + ```{note} + Account must be an available resource + ``` + """ @property def total_assets_created(self) -> UInt64: - """The number of existing ASAs created by this account.""" + """The number of existing ASAs created by this account. + + ```{note} + Account must be an available resource + ``` + """ @property def total_assets(self) -> UInt64: - """The numbers of ASAs held by this account (including ASAs this account created).""" + """The numbers of ASAs held by this account (including ASAs this account created). + + ```{note} + Account must be an available resource + ``` + """ @property def total_boxes(self) -> UInt64: - """The number of existing boxes created by this account's app.""" + """The number of existing boxes created by this account's app. + + ```{note} + Account must be an available resource + ``` + """ @property def total_box_bytes(self) -> UInt64: - """The total number of bytes used by this account's app's box keys and values.""" - -class Asset: - """An Asset on the Algorand network. + """The total number of bytes used by this account's app's box keys and values. - Note: must be an available resource to access properties other than `asset_id` - """ + ```{note} + Account must be an available resource + ``` + """ - def __init__(self, asset_id: UInt64 | int): ... - @property - def asset_id(self) -> UInt64: ... - def __eq__(self, other: Asset) -> bool: ... # type: ignore[override] - def __ne__(self, other: Asset) -> bool: ... # type: ignore[override] +class Asset: + """An Asset on the Algorand network.""" + def __init__(self, asset_id: UInt64 | int): + """Initialized with the id of an asset""" + @property + def asset_id(self) -> UInt64: + """Returns the id of the Asset""" + def __eq__(self, other: Asset) -> bool: # type: ignore[override] + """Asset equality is determined by the equality of an Asset's id""" + def __ne__(self, other: Asset) -> bool: # type: ignore[override] + """Asset equality is determined by the equality of an Asset's id""" # truthiness - def __bool__(self) -> bool: ... # returns True iff asset_id > 0 + def __bool__(self) -> bool: + """Returns `True` if `asset_id` is not `0`""" @property def total(self) -> UInt64: - """Total number of units of this asset""" + """Total number of units of this asset + + ```{note} + Asset must be an available resource + ``` + """ @property def decimals(self) -> UInt64: - """See AssetParams.Decimals""" + """See AssetParams.Decimals + + ```{note} + Asset must be an available resource + ``` + """ @property def default_frozen(self) -> bool: - """Frozen by default or not""" + """Frozen by default or not + + ```{note} + Asset must be an available resource + ``` + """ @property def unit_name(self) -> Bytes: - """Asset unit name""" + """Asset unit name + + ```{note} + Asset must be an available resource + ``` + """ @property def name(self) -> Bytes: - """Asset name""" + """Asset name + + ```{note} + Asset must be an available resource + ``` + """ @property def url(self) -> Bytes: - """URL with additional info about the asset""" + """URL with additional info about the asset + + ```{note} + Asset must be an available resource + ``` + """ @property def metadata_hash(self) -> Bytes: - """Arbitrary commitment""" + """Arbitrary commitment + + ```{note} + Asset must be an available resource + ``` + """ @property def manager(self) -> Account: - """Manager address""" + """Manager address + + ```{note} + Asset must be an available resource + ``` + """ @property def reserve(self) -> Account: - """Reserve address""" + """Reserve address + + ```{note} + Asset must be an available resource + ``` + """ @property def freeze(self) -> Account: - """Freeze address""" + """Freeze address + + ```{note} + Asset must be an available resource + ``` + """ @property def clawback(self) -> Account: - """Clawback address""" + """Clawback address + + ```{note} + Asset must be an available resource + ``` + """ @property def creator(self) -> Account: - """Creator address""" + """Creator address + + ```{note} + Asset must be an available resource + ``` + """ def balance(self, account: Account) -> UInt64: - """Amount of the asset unit held by this account""" - def frozen(self, account: Account) -> bool: - """Is the asset frozen or not""" + """Amount of the asset unit held by this account -class Application: - """An Application on the Algorand network. + ```{note} + Asset and supplied Account must be an available resource + ``` + """ + def frozen(self, account: Account) -> bool: + """Is the asset frozen or not - Note: must be an available resource to access properties other than `application_id` - """ + ```{note} + Asset and supplied Account must be an available resource + ``` + """ - def __init__(self, application_id: UInt64 | int): ... - @property - def application_id(self) -> UInt64: ... - def __eq__(self, other: Application) -> bool: ... # type: ignore[override] - def __ne__(self, other: Application) -> bool: ... # type: ignore[override] +class Application: + """An Application on the Algorand network.""" + def __init__(self, application_id: UInt64 | int): + """Initialized with the id of an application""" + @property + def application_id(self) -> UInt64: + """Returns the id of the application""" + def __eq__(self, other: Application) -> bool: # type: ignore[override] + """Application equality is determined by the equality of an Application's id""" + def __ne__(self, other: Application) -> bool: # type: ignore[override] + """Application equality is determined by the equality of an Application's id""" # truthiness - def __bool__(self) -> bool: ... # returns True iff application_id > 0 + def __bool__(self) -> bool: + """Returns `True` if `application_id` is not `0`""" @property def approval_program(self) -> Bytes: - """Bytecode of Approval Program""" + """Bytecode of Approval Program + + ```{note} + Application must be an available resource + ``` + """ @property def clear_state_program(self) -> Bytes: - """Bytecode of Clear State Program""" + """Bytecode of Clear State Program + + ```{note} + Application must be an available resource + ``` + """ @property def global_num_uint(self) -> UInt64: - """Number of uint64 values allowed in Global State""" + """Number of uint64 values allowed in Global State + + ```{note} + Application must be an available resource + ``` + """ @property def global_num_byte_slice(self) -> UInt64: - """Number of byte array values allowed in Global State""" + """Number of byte array values allowed in Global State + + ```{note} + Application must be an available resource + ``` + """ @property def local_num_uint(self) -> UInt64: - """Number of uint64 values allowed in Local State""" + """Number of uint64 values allowed in Local State + + ```{note} + Application must be an available resource + ``` + """ @property def local_num_byte_slice(self) -> UInt64: - """Number of byte array values allowed in Local State""" + """Number of byte array values allowed in Local State + + ```{note} + Application must be an available resource + ``` + """ @property def extra_program_pages(self) -> UInt64: - """Number of Extra Program Pages of code space""" + """Number of Extra Program Pages of code space + + ```{note} + Application must be an available resource + ``` + """ @property def creator(self) -> Account: - """Creator address""" + """Creator address + + ```{note} + Application must be an available resource + ``` + """ @property def address(self) -> Account: - """Address for which this application has authority""" + """Address for which this application has authority + + ```{note} + Application must be an available resource + ``` + """ diff --git a/src/puyapy-stubs/_state.pyi b/src/puyapy-stubs/_state.pyi index 4824cbca87..75d6f2d54c 100644 --- a/src/puyapy-stubs/_state.pyi +++ b/src/puyapy-stubs/_state.pyi @@ -7,28 +7,117 @@ _T = typing.TypeVar("_T") class LocalState(typing.Generic[_T]): """Local state associated with the application and an account""" - def __init__(self, type_: type[_T], /) -> None: ... - def __getitem__(self, account: Account | UInt64 | int) -> _T: ... - def __setitem__(self, account: Account | UInt64 | int, value: _T) -> None: ... - def __delitem__(self, account: Account | UInt64 | int) -> None: ... - def __contains__(self, account: Account | UInt64 | int) -> bool: ... - def get(self, account: Account | UInt64 | int, default: _T) -> _T: ... - def maybe(self, account: Account | UInt64 | int) -> tuple[_T, bool]: ... + def __init__(self, type_: type[_T], /) -> None: + """Must be initialized with the type that will be stored + + ```python + self.names = LocalState(puyapy.Bytes) + ``` + """ + def __getitem__(self, account: Account | UInt64 | int) -> _T: + """Data can be accessed by an `Account` reference or foreign account index + + ```python + account_name = self.names[account] + ``` + """ + def __setitem__(self, account: Account | UInt64 | int, value: _T) -> None: + """Data can be stored by using an `Account` reference or foreign account index + + ```python + self.names[account] = account_name + ``` + """ + def __delitem__(self, account: Account | UInt64 | int) -> None: + """Data can be removed by using an `Account` reference or foreign account index + + ```python + del self.names[account] + ``` + """ + def __contains__(self, account: Account | UInt64 | int) -> bool: + """Can test if data exists by using an `Account` reference or foreign account index + + ```python + assert account in self.names + ``` + """ + def get(self, account: Account | UInt64 | int, default: _T) -> _T: + """Can retrieve value using an `Account` reference or foreign account index, + and a fallback default value. + + ```python + name = self.names.get(account, Bytes(b"no name") + ``` + """ + def maybe(self, account: Account | UInt64 | int) -> tuple[_T, bool]: + """Can retrieve value, and a bool indicating if the value was present + using an `Account` reference or foreign account index. + + ```python + name, name_exists = self.names.maybe(account) + if not name_exists: + name = Bytes(b"no name") + ``` + """ class GlobalState(typing.Generic[_T]): - """Global state associated with the application""" + """Global state associated with the application, the key will be the name of the member, this + is assigned to + + ```{note} + The `GlobalState` class provides a richer API that in addition to storing and retrieving + values, can test if a value is set or unset it. However if this extra functionality is not + needed then it is simpler to just store the data without the GlobalState proxy + e.g. `self.some_variable = UInt64(0)` + ``` + """ @typing.overload - def __init__(self, type_: type[_T], /) -> None: ... + def __init__(self, type_: type[_T], /) -> None: + """Can be initialized with the type of the value to store, with the value unset""" @typing.overload - def __init__(self, initial_value: _T, /) -> None: ... + def __init__(self, initial_value: _T, /) -> None: + """Can be initialized with an initial value""" @property - def value(self) -> _T: ... + def value(self) -> _T: + """Returns the value or and error if the value is not set + + ```python + name = self.name.value + ``` + """ @value.setter - def value(self, value: _T) -> None: ... + def value(self, value: _T) -> None: + """Sets the value + + ```python + self.name.value = Bytes(b"Alice") + ``` + """ @value.deleter - def value(self) -> None: ... - def __bool__(self) -> bool: ... - """Returns True if the key has a value set, regardless of the truthiness of that value""" - def get(self, default: _T) -> _T: ... - def maybe(self) -> tuple[_T, bool]: ... + def value(self) -> None: + """Removes the value + + ```python + del self.name.value + ``` + """ + def __bool__(self) -> bool: + """Returns `True` if the key has a value set, regardless of the truthiness of that value""" + def get(self, default: _T) -> _T: + """Returns the value or `default` if no value is set + + ```python + name = self.name.get(Bytes(b"no name") + ``` + """ + def maybe(self) -> tuple[_T, bool]: + """Returns the value, and a bool + + ```python + name, name_exists = self.name.maybe() + if not name_exists: + name = Bytes(b"no name") + ``` + """ diff --git a/src/puyapy-stubs/_util.pyi b/src/puyapy-stubs/_util.pyi index ed52577bb5..bb880d2fb3 100644 --- a/src/puyapy-stubs/_util.pyi +++ b/src/puyapy-stubs/_util.pyi @@ -1,20 +1,16 @@ -from enum import Enum - from puyapy import UInt64 -class OpUpFeeSource(Enum): - """An Enum object that defines the source for fees for the OpUp utility.""" +class OpUpFeeSource(UInt64): + """Defines the source of fees for the OpUp utility.""" - #: Only the excess fee (credit) on the outer group should be used (set inner_tx.fee=0) - GroupCredit = 0 - #: The app's account will cover all fees (set inner_tx.fee=Global.min_tx_fee()) - AppAccount = 1 - #: First the excess will be used, remaining fees will be taken from the app account - Any = 2 + GroupCredit: OpUpFeeSource = ... + """Only the excess fee (credit) on the outer group should be used (set inner_tx.fee=0)""" + AppAccount: OpUpFeeSource = ... + """The app's account will cover all fees (set inner_tx.fee=Global.min_tx_fee())""" + Any: OpUpFeeSource = ... + """First the excess will be used, remaining fees will be taken from the app account""" def ensure_budget( required_budget: UInt64 | int, fee_source: OpUpFeeSource = OpUpFeeSource.Any ) -> None: - """ - Ensure the available op code budget is greater than or equal to required_budget - """ + """Ensure the available op code budget is greater than or equal to required_budget""" diff --git a/src/puyapy-stubs/arc4.pyi b/src/puyapy-stubs/arc4.pyi index fcdf6d131e..bbb7dc8809 100644 --- a/src/puyapy-stubs/arc4.pyi +++ b/src/puyapy-stubs/arc4.pyi @@ -1,5 +1,5 @@ import typing -from collections.abc import Callable, Iterable, Mapping, Sequence, Reversible +from collections.abc import Callable, Iterable, Mapping, Reversible, Sequence import puyapy @@ -25,9 +25,7 @@ _TABIDefaultArgSource: typing.TypeAlias = object # if we use type aliasing here for Callable[_P, _R], mypy thinks it involves Any... @typing.overload -def abimethod(fn: Callable[_P, _R], /) -> Callable[_P, _R]: - """Indicates a method is an ARC4 ABI method""" - +def abimethod(fn: Callable[_P, _R], /) -> Callable[_P, _R]: ... @typing.overload def abimethod( *, @@ -37,19 +35,17 @@ def abimethod( readonly: bool = False, default_args: Mapping[str, str | _TABIDefaultArgSource] | None = None, ) -> Callable[[Callable[_P, _R]], Callable[_P, _R],]: - """Indicates a method is an ARC4 ABI method""" + """Decorator that indicates a method is an ARC4 ABI method""" @typing.overload -def baremethod(fn: Callable[_P, _R], /) -> Callable[_P, _R]: - """Indicates a method is an ARC4 bare method""" - +def baremethod(fn: Callable[_P, _R], /) -> Callable[_P, _R]: ... @typing.overload def baremethod( *, allow_actions: AllowedOnCompletes | None = None, create: bool | typing.Literal["allow"] = False, ) -> Callable[[Callable[_P, _R]], Callable[_P, _R],]: - """Indicates a method is an ARC4 bare method""" + """Decorator that indicates a method is an ARC4 bare method""" _T = typing.TypeVar("_T") @@ -147,7 +143,7 @@ UInt512: typing.TypeAlias = BigUIntN[typing.Literal[512]] class Bool(_ABIEncoded[bool]): """An ARC4 encoded bool""" - def __init__(self, value: bool) -> None: ... + def __init__(self, value: bool) -> None: ... # noqa: FBT001 _TArrayItem = typing.TypeVar("_TArrayItem") _TArrayLength = typing.TypeVar("_TArrayLength", bound=int) @@ -324,7 +320,7 @@ class Tuple( ) class _StructMeta(type): def __new__( - mcs, + cls, name: str, bases: tuple[type, ...], namespace: dict[str, object],