From cf7e91ea31fbf8a5eb53546834b1f82c5f3aba62 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 18:49:01 +0000 Subject: [PATCH 01/39] feat(test): test contract parsing --- tests/test_generator.py | 19 +++++++++++++++++++ tests/test_parser.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tests/test_generator.py b/tests/test_generator.py index f717547..c57a075 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -73,3 +73,22 @@ def test_contract_rst_generation(contracts_dir: Path, output_dir: Path) -> None: # Check docstrings assert "Transfer tokens to a specified address" in content assert "Get the token balance of an account" in content + + +def test_generate_struct_docs(): + """Test struct documentation generation.""" + struct = Struct( + name="MyStruct", + fields=[ + Parameter(name="field1", type="uint256"), + Parameter(name="field2", type="address"), + ], + ) + expected_output = ( + ".. py:class:: MyStruct\n\n" + " .. py:attribute:: MyStruct.field1\n\n" + " uint256\n" + " .. py:attribute:: MyStruct.field2\n\n" + " address" + ) + assert SphinxGenerator._generate_struct_docs(struct) == expected_output diff --git a/tests/test_parser.py b/tests/test_parser.py index 85d984e..f482313 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -82,3 +82,36 @@ def test_empty_contract(tmp_path: Path) -> None: assert contracts[0].name == "empty" assert contracts[0].docstring is None assert len(contracts[0].functions) == 0 + + +def test_extract_contract_docstring(): + """Test contract docstring extraction.""" + content = '''""" + This is a contract docstring. + """ + @external + def foo() -> bool: + pass + ''' + parser = VyperParser(Path(".")) + docstring = parser._extract_contract_docstring(content) + assert docstring == "This is a contract docstring." + + +def test_extract_structs(): + """Test struct extraction from contract.""" + content = """ + struct MyStruct { + field1: uint256 + field2: address + } + """ + parser = VyperParser(Path(".")) + structs = parser._extract_structs(content) + assert len(structs) == 1 + assert structs[0].name == "MyStruct" + assert len(structs[0].fields) == 2 + assert structs[0].fields[0].name == "field1" + assert structs[0].fields[0].type == "uint256" + assert structs[0].fields[1].name == "field2" + assert structs[0].fields[1].type == "address" From 5ab9047e919be03cc273ab84e7d26a0b1c35ae20 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 18:52:00 +0000 Subject: [PATCH 02/39] fix(test): fix tests --- sphinx_autodoc_vyper/generator.py | 2 +- tests/test_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index 6a59519..01c92f7 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -100,7 +100,7 @@ def _generate_function_docs(func: Function) -> str: @staticmethod def _generate_struct_docs(struct: Struct) -> str: - content = f".. py:class:: {func.name}\n\n" + content = f".. py:class:: {struct.name}\n\n" for field in struct.fields: content += f" .. py:attribute:: {struct.name}.{field.name}\n\n" content += f" {field.type}" diff --git a/tests/test_generator.py b/tests/test_generator.py index c57a075..9af088a 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -3,7 +3,7 @@ from pathlib import Path from sphinx_autodoc_vyper.generator import SphinxGenerator -from sphinx_autodoc_vyper.parser import VyperParser +from sphinx_autodoc_vyper.parser import Parameter, Struct, VyperParser def test_sphinx_generation(contracts_dir: Path, output_dir: Path) -> None: From 533f165566fb916eec74a3b632888aa561846ed2 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 18:53:54 +0000 Subject: [PATCH 03/39] fix(mypy): fix type errs --- tests/test_generator.py | 2 +- tests/test_parser.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_generator.py b/tests/test_generator.py index 9af088a..18901a1 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -75,7 +75,7 @@ def test_contract_rst_generation(contracts_dir: Path, output_dir: Path) -> None: assert "Get the token balance of an account" in content -def test_generate_struct_docs(): +def test_generate_struct_docs() -> None: """Test struct documentation generation.""" struct = Struct( name="MyStruct", diff --git a/tests/test_parser.py b/tests/test_parser.py index f482313..1dad895 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -84,7 +84,7 @@ def test_empty_contract(tmp_path: Path) -> None: assert len(contracts[0].functions) == 0 -def test_extract_contract_docstring(): +def test_extract_contract_docstring() -> None: """Test contract docstring extraction.""" content = '''""" This is a contract docstring. @@ -98,7 +98,7 @@ def foo() -> bool: assert docstring == "This is a contract docstring." -def test_extract_structs(): +def test_extract_structs() -> None: """Test struct extraction from contract.""" content = """ struct MyStruct { From ebbd631047eb318c0b8b954a57e840448153e440 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 18:56:25 +0000 Subject: [PATCH 04/39] feat(test): test more complex contract --- tests/test_generator.py | 52 +++++++++++++++++++++++++++++++ tests/test_parser.py | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/tests/test_generator.py b/tests/test_generator.py index 18901a1..14ff45d 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -92,3 +92,55 @@ def test_generate_struct_docs() -> None: " address" ) assert SphinxGenerator._generate_struct_docs(struct) == expected_output + + +def test_generate_docs_for_contract_with_functions_and_structs() -> None: + """Test documentation generation for a contract with both functions and structs.""" + contract = Contract( + name="TestContract", + docstring="This is a contract docstring.", + structs=[ + Struct( + name="MyStruct", + fields=[ + Parameter(name="field1", type="uint256"), + Parameter(name="field2", type="address"), + ], + ) + ], + functions=[ + Function( + name="transfer", + params=[ + Parameter(name="to", type="address"), + Parameter(name="amount", type="uint256"), + ], + return_type="bool", + docstring="Transfer tokens to a specified address.", + ), + Function( + name="balance_of", + params=[Parameter(name="owner", type="address")], + return_type="uint256", + docstring="Get the balance of an account.", + ), + ], + ) + + generator = SphinxGenerator(".") + docs = generator.generate([contract]) + + # Expected documentation output + expected_docs = ( + ".. py:class:: MyStruct\n\n" + " .. py:attribute:: MyStruct.field1\n\n" + " uint256\n" + " .. py:attribute:: MyStruct.field2\n\n" + " address\n\n" + ".. py:function:: transfer(to: address, amount: uint256) -> bool\n\n" + " Transfer tokens to a specified address.\n\n" + ".. py:function:: balance_of(owner: address) -> uint256\n\n" + " Get the balance of an account.\n\n" + ) + + assert docs == expected_docs diff --git a/tests/test_parser.py b/tests/test_parser.py index 1dad895..3c7470b 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -115,3 +115,71 @@ def test_extract_structs() -> None: assert structs[0].fields[0].type == "uint256" assert structs[0].fields[1].name == "field2" assert structs[0].fields[1].type == "address" + + +def test_parse_contract_with_functions_and_structs() -> None: + """Test parsing a contract with both functions and structs.""" + contract_content = ''' + """ + This is a contract docstring. + """ + + struct MyStruct: + field1: uint256 + field2: address + + @external + def transfer(to: address, amount: uint256) -> bool: + """ + Transfer tokens to a specified address. + """ + return True + + @external + def balance_of(owner: address) -> uint256: + """ + Get the balance of an account. + """ + return 0 + ''' + + parser = VyperParser(Path(".")) + parser._extract_contract_docstring = lambda x: "This is a contract docstring." + parser._extract_structs = lambda x: [ + Struct( + name="MyStruct", + fields=[ + Parameter(name="field1", type="uint256"), + Parameter(name="field2", type="address"), + ], + ) + ] + parser._extract_functions = lambda x: [ + Function( + name="transfer", + params=[ + Parameter(name="to", type="address"), + Parameter(name="amount", type="uint256"), + ], + return_type="bool", + docstring="Transfer tokens to a specified address.", + ), + Function( + name="balance_of", + params=[Parameter(name="owner", type="address")], + return_type="uint256", + docstring="Get the balance of an account.", + ), + ] + + contract = parser.parse_contracts()[0] + + # Assertions for structs + assert len(contract.structs) == 1 + assert contract.structs[0].name == "MyStruct" + assert len(contract.structs[0].fields) == 2 + + # Assertions for functions + assert len(contract.functions) == 2 + assert contract.functions[0].name == "transfer" + assert contract.functions[1].name == "balance_of" From 7d6c21677852a43099807dc681a3d70e4b16a3bf Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 18:58:57 +0000 Subject: [PATCH 05/39] feat: Tuple length --- sphinx_autodoc_vyper/parser.py | 3 +++ tests/test_parser.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index ec80d44..1e3ec71 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -38,6 +38,9 @@ def __post_init__(self) -> None: for type in self.types: if type not in VALID_VYPER_TYPES: logger.warning(f"{self} is not a valid Vyper type") + + def __len__(self) -> int: + return len(self.types) @dataclass diff --git a/tests/test_parser.py b/tests/test_parser.py index 3c7470b..633a477 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -183,3 +183,12 @@ def balance_of(owner: address) -> uint256: assert len(contract.functions) == 2 assert contract.functions[0].name == "transfer" assert contract.functions[1].name == "balance_of" + + +def test_tuple_length() -> None: + """Test the length of a Tuple instance.""" + tuple_instance = Tuple(types=["uint256", "address", "bool"]) + assert len(tuple_instance) == 3 + + empty_tuple_instance = Tuple(types=[]) + assert len(empty_tuple_instance) == 0 From 445da9925b72ecffa8221fff14c8bc22b3bc104b Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 18:59:42 +0000 Subject: [PATCH 06/39] chore: black . --- sphinx_autodoc_vyper/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 1e3ec71..dfda1cf 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -38,7 +38,7 @@ def __post_init__(self) -> None: for type in self.types: if type not in VALID_VYPER_TYPES: logger.warning(f"{self} is not a valid Vyper type") - + def __len__(self) -> int: return len(self.types) From 826625ccb38103ad760f2e38759897b711672523 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:01:23 +0000 Subject: [PATCH 07/39] fix: broken imports --- tests/test_generator.py | 2 +- tests/test_parser.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_generator.py b/tests/test_generator.py index 14ff45d..4b18dbe 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -3,7 +3,7 @@ from pathlib import Path from sphinx_autodoc_vyper.generator import SphinxGenerator -from sphinx_autodoc_vyper.parser import Parameter, Struct, VyperParser +from sphinx_autodoc_vyper.parser import Contract, Function, Parameter, Struct, VyperParser def test_sphinx_generation(contracts_dir: Path, output_dir: Path) -> None: diff --git a/tests/test_parser.py b/tests/test_parser.py index 633a477..b7c80e6 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -2,7 +2,7 @@ from pathlib import Path -from sphinx_autodoc_vyper.parser import Contract, Function, Parameter, VyperParser +from sphinx_autodoc_vyper.parser import Contract, Function, Parameter, Tuple, VyperParser def test_parse_contracts(contracts_dir: Path) -> None: @@ -154,7 +154,7 @@ def balance_of(owner: address) -> uint256: ], ) ] - parser._extract_functions = lambda x: [ + parser._extract_functions = lambda x: [ # type: ignore [method-assign] Function( name="transfer", params=[ From d42b4505219fe0873010535c60c69da611cf3e76 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:02:18 +0000 Subject: [PATCH 08/39] chore: black . --- tests/test_generator.py | 8 +++++++- tests/test_parser.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_generator.py b/tests/test_generator.py index 4b18dbe..f28a118 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -3,7 +3,13 @@ from pathlib import Path from sphinx_autodoc_vyper.generator import SphinxGenerator -from sphinx_autodoc_vyper.parser import Contract, Function, Parameter, Struct, VyperParser +from sphinx_autodoc_vyper.parser import ( + Contract, + Function, + Parameter, + Struct, + VyperParser, +) def test_sphinx_generation(contracts_dir: Path, output_dir: Path) -> None: diff --git a/tests/test_parser.py b/tests/test_parser.py index b7c80e6..533ce6d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -2,7 +2,13 @@ from pathlib import Path -from sphinx_autodoc_vyper.parser import Contract, Function, Parameter, Tuple, VyperParser +from sphinx_autodoc_vyper.parser import ( + Contract, + Function, + Parameter, + Tuple, + VyperParser, +) def test_parse_contracts(contracts_dir: Path) -> None: From bbdea85a25a2a924b66d60d695277b926a3e59d8 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:05:41 +0000 Subject: [PATCH 09/39] fix: missing arg --- tests/test_generator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_generator.py b/tests/test_generator.py index f28a118..f12db1f 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -104,6 +104,7 @@ def test_generate_docs_for_contract_with_functions_and_structs() -> None: """Test documentation generation for a contract with both functions and structs.""" contract = Contract( name="TestContract", + path=".", docstring="This is a contract docstring.", structs=[ Struct( From 2f8c0dce72bc5d1e7ba28ced456760c855f3fc9b Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:07:42 +0000 Subject: [PATCH 10/39] fix(mypy): fix type errs --- tests/test_parser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 533ce6d..4a8f1d6 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -6,6 +6,7 @@ Contract, Function, Parameter, + Struct, Tuple, VyperParser, ) @@ -150,8 +151,8 @@ def balance_of(owner: address) -> uint256: ''' parser = VyperParser(Path(".")) - parser._extract_contract_docstring = lambda x: "This is a contract docstring." - parser._extract_structs = lambda x: [ + parser._extract_contract_docstring = lambda x: "This is a contract docstring." # type: ignore [method-assign,assignment] + parser._extract_structs = lambda x: [ # type: ignore [method-assign,assignment] Struct( name="MyStruct", fields=[ @@ -160,7 +161,7 @@ def balance_of(owner: address) -> uint256: ], ) ] - parser._extract_functions = lambda x: [ # type: ignore [method-assign] + parser._extract_functions = lambda x: [ # type: ignore [method-assign,assignment] Function( name="transfer", params=[ From 353a13625c1ea8f6667701d10ae12340ef37964e Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:12:18 +0000 Subject: [PATCH 11/39] fix: unused var --- tests/test_parser.py | 46 ++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 4a8f1d6..f133b62 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -151,33 +151,10 @@ def balance_of(owner: address) -> uint256: ''' parser = VyperParser(Path(".")) - parser._extract_contract_docstring = lambda x: "This is a contract docstring." # type: ignore [method-assign,assignment] - parser._extract_structs = lambda x: [ # type: ignore [method-assign,assignment] - Struct( - name="MyStruct", - fields=[ - Parameter(name="field1", type="uint256"), - Parameter(name="field2", type="address"), - ], - ) - ] - parser._extract_functions = lambda x: [ # type: ignore [method-assign,assignment] - Function( - name="transfer", - params=[ - Parameter(name="to", type="address"), - Parameter(name="amount", type="uint256"), - ], - return_type="bool", - docstring="Transfer tokens to a specified address.", - ), - Function( - name="balance_of", - params=[Parameter(name="owner", type="address")], - return_type="uint256", - docstring="Get the balance of an account.", - ), - ] + # Use the actual content to parse + parser._extract_contract_docstring = lambda x: parser._extract_contract_docstring(contract_content) # type: ignore [method-assign,assignment] + parser._extract_structs = lambda x: parser._extract_structs(contract_content) # type: ignore [method-assign,assignment] + parser._extract_functions = lambda x: parser._extract_functions(contract_content) # type: ignore [method-assign,assignment] contract = parser.parse_contracts()[0] @@ -185,11 +162,26 @@ def balance_of(owner: address) -> uint256: assert len(contract.structs) == 1 assert contract.structs[0].name == "MyStruct" assert len(contract.structs[0].fields) == 2 + assert contract.structs[0].fields[0].name == "field1" + assert contract.structs[0].fields[0].type == "uint256" + assert contract.structs[0].fields[1].name == "field2" + assert contract.structs[0].fields[1].type == "address" # Assertions for functions assert len(contract.functions) == 2 assert contract.functions[0].name == "transfer" + assert len(contract.functions[0].params) == 2 + assert contract.functions[0].params[0].name == "to" + assert contract.functions[0].params[0].type == "address" + assert contract.functions[0].params[1].name == "amount" + assert contract.functions[0].params[1].type == "uint256" + assert contract.functions[0].return_type == "bool" + assert contract.functions[1].name == "balance_of" + assert len(contract.functions[1].params) == 1 + assert contract.functions[1].params[0].name == "owner" + assert contract.functions[1].params[0].type == "address" + assert contract.functions[1].return_type == "uint256" def test_tuple_length() -> None: From 71311bfe267e3607cc0bd49ec56cb02cf7cc75e6 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:14:05 +0000 Subject: [PATCH 12/39] chore: ruff --- tests/test_parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index f133b62..fa34cc3 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -6,7 +6,6 @@ Contract, Function, Parameter, - Struct, Tuple, VyperParser, ) From 105aa59aba98b9e330c38ddcd2b7db674d30b6ef Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:18:25 +0000 Subject: [PATCH 13/39] fix: struct formatting --- sphinx_autodoc_vyper/generator.py | 2 +- tests/test_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index 01c92f7..5792c01 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -103,5 +103,5 @@ def _generate_struct_docs(struct: Struct) -> str: content = f".. py:class:: {struct.name}\n\n" for field in struct.fields: content += f" .. py:attribute:: {struct.name}.{field.name}\n\n" - content += f" {field.type}" + content += f" {field.type}\n\n" return content diff --git a/tests/test_generator.py b/tests/test_generator.py index f12db1f..d0e4ce8 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -141,7 +141,7 @@ def test_generate_docs_for_contract_with_functions_and_structs() -> None: expected_docs = ( ".. py:class:: MyStruct\n\n" " .. py:attribute:: MyStruct.field1\n\n" - " uint256\n" + " uint256\n\n" " .. py:attribute:: MyStruct.field2\n\n" " address\n\n" ".. py:function:: transfer(to: address, amount: uint256) -> bool\n\n" From b881e9daf69efff341586df121766dfe62d90529 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:21:41 +0000 Subject: [PATCH 14/39] fix(test): fix test_parser --- tests/test_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_parser.py b/tests/test_parser.py index fa34cc3..773cae6 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -151,6 +151,7 @@ def balance_of(owner: address) -> uint256: parser = VyperParser(Path(".")) # Use the actual content to parse + parser.parse_contracts = lambda x: [parser._parse_contract(contract_content)] parser._extract_contract_docstring = lambda x: parser._extract_contract_docstring(contract_content) # type: ignore [method-assign,assignment] parser._extract_structs = lambda x: parser._extract_structs(contract_content) # type: ignore [method-assign,assignment] parser._extract_functions = lambda x: parser._extract_functions(contract_content) # type: ignore [method-assign,assignment] From ab4718afe9118bae44281591f4a7f9b0bcc0188c Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 19:24:01 +0000 Subject: [PATCH 15/39] fix(test): fix test_parser --- tests/test_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 773cae6..8a6252f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -151,7 +151,7 @@ def balance_of(owner: address) -> uint256: parser = VyperParser(Path(".")) # Use the actual content to parse - parser.parse_contracts = lambda x: [parser._parse_contract(contract_content)] + parser.parse_contracts = lambda: [parser._parse_contract(contract_content)] parser._extract_contract_docstring = lambda x: parser._extract_contract_docstring(contract_content) # type: ignore [method-assign,assignment] parser._extract_structs = lambda x: parser._extract_structs(contract_content) # type: ignore [method-assign,assignment] parser._extract_functions = lambda x: parser._extract_functions(contract_content) # type: ignore [method-assign,assignment] From a6471851be8732384daa3eb83f1d235631108dd0 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 21:24:51 +0000 Subject: [PATCH 16/39] fix: Structs section header --- sphinx_autodoc_vyper/generator.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index 5792c01..78385ff 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -72,12 +72,12 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: content += f"{contract.docstring}\n\n" if contract.structs: - content += "Structs\n---------\n\n" + content += _insert_content_section("Structs") for struct in contract.structs: content += self._generate_struct_docs(struct) if contract.functions: - content += "Functions\n---------\n\n" + content += _insert_content_section("Functions") for func in contract.functions: content += self._generate_function_docs(func) @@ -105,3 +105,7 @@ def _generate_struct_docs(struct: Struct) -> str: content += f" .. py:attribute:: {struct.name}.{field.name}\n\n" content += f" {field.type}\n\n" return content + +def _insert_content_section(name: str) -> str: + """Insert a hyperlinked content section accessible from the docs index.""" + return f"{name}\n{'-' * len(name)}\n\n" From a64715ebf7323ff5f8c6e15c972916f3142b2f46 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 21:36:27 +0000 Subject: [PATCH 17/39] chore: refactor regex --- sphinx_autodoc_vyper/parser.py | 39 ++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index dfda1cf..92f5ddb 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -13,6 +13,11 @@ valid_uints = {f"uint{8 * (i+1)}" for i in range(32)} VALID_VYPER_TYPES = {*valid_ints, *valid_uints, "address", "bool", "Bytes", "String"} +CONSTANT_PATTERN = r"(\w+):\s*constant\((\w+)\)\s*=\s*(.*?)$" +STRUCT_PATTERN = r"struct\s+(\w+)\s*{([^}]*)}" +FUNCTION_PATTERN = r'@(external|internal)\s+def\s+([^(]+)\(([^)]*)\)(\s*->\s*[^:]+)?:\s*("""[\s\S]*?""")?' +PARAM_PATTERN = r"(\w+:\s*DynArray\[[^\]]+\]|\w+:\s*\w+)" + @dataclass class Constant: @@ -164,26 +169,33 @@ def _extract_contract_docstring(self, content: str) -> Optional[str]: match = re.search(r'^"""(.*?)"""', content, re.DOTALL | re.MULTILINE) return match.group(1).strip() if match else None + def _extract_constants(self, content: str) -> List[Constant]: + """Extract constants from the contract.""" + return [ + Constant( + name=match.group(1).strip(), + type=match.group(2).strip(), + value=match.group(3).strip(), + ) + for match in re.finditer(CONSTANT_PATTERN, content, re.MULTILINE) + ] + def _extract_structs(self, content: str) -> List[Struct]: """Extract all structs from the contract.""" - structs = [] - struct_pattern = r"struct\s+(\w+)\s*{([^}]*)}" - - for match in re.finditer(struct_pattern, content): - name = match.group(1).strip() - fields_str = match.group(2).strip() - fields = self._parse_params(fields_str) - structs.append(Struct(name=name, fields=fields)) - - return structs + return [ + Struct( + name=match.group(1).strip(), + fields=self._parse_params(match.group(2).strip()), + ) + for match in re.finditer(STRUCT_PATTERN, content) + ] def _extract_functions(self, content: str) -> List[Function]: """Extract all functions from the contract, with @external functions listed first.""" external_functions = [] internal_functions = [] - function_pattern = r'@(external|internal)\s+def\s+([^(]+)\(([^)]*)\)(\s*->\s*[^:]+)?:\s*("""[\s\S]*?""")?' - for match in re.finditer(function_pattern, content): + for match in re.finditer(FUNCTION_PATTERN, content): decorator = match.group(1).strip() name = match.group(2).strip() params_str = match.group(3).strip() @@ -216,8 +228,7 @@ def _parse_params(params_str: str) -> List[Parameter]: params = [] # Use regex to split by commas that are not within brackets - param_pattern = r"(\w+:\s*DynArray\[[^\]]+\]|\w+:\s*\w+)" - for param in re.finditer(param_pattern, params_str): + for param in re.finditer(PARAM_PATTERN, params_str): name, type_str = param.group().split(":") type_str = type_str.strip() typ = Tuple(type_str[1:-1].split(",")) if type_str[1] == "(" else type_str From 4c3a5b98beabfd1d74b051ea50985d6413416495 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 21:37:26 +0000 Subject: [PATCH 18/39] feat: use http.server instead of socketserver --- sphinx_autodoc_vyper/generator.py | 1 + sphinx_autodoc_vyper/server.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index 78385ff..865ef48 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -106,6 +106,7 @@ def _generate_struct_docs(struct: Struct) -> str: content += f" {field.type}\n\n" return content + def _insert_content_section(name: str) -> str: """Insert a hyperlinked content section accessible from the docs index.""" return f"{name}\n{'-' * len(name)}\n\n" diff --git a/sphinx_autodoc_vyper/server.py b/sphinx_autodoc_vyper/server.py index 910b2ac..4dfc518 100644 --- a/sphinx_autodoc_vyper/server.py +++ b/sphinx_autodoc_vyper/server.py @@ -2,7 +2,6 @@ import http.server import os -import socketserver import webbrowser from pathlib import Path from typing import NoReturn @@ -20,9 +19,10 @@ def serve_docs(build_dir: Path, port: int = 8000) -> NoReturn: # type: ignore [ os.chdir(html_dir) - handler = http.server.SimpleHTTPRequestHandler - - with socketserver.TCPServer(("", port), handler) as httpd: + with http.server.HTTPServer( + ("", port), + http.server.SimpleHTTPRequestHandler, + ) as httpd: url = f"http://localhost:{port}" print(f"Serving documentation at {url}") webbrowser.open(url) From bae1e35e1934ebbf1d8fb0c8adbfa5bd26f93e46 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:13:20 +0000 Subject: [PATCH 19/39] feat: document enums and events --- sphinx_autodoc_vyper/generator.py | 21 +++++++ sphinx_autodoc_vyper/parser.py | 91 ++++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index 865ef48..fa0641e 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -71,11 +71,32 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: if contract.docstring: content += f"{contract.docstring}\n\n" + if contract.enums: + content += _insert_content_section("Enums") + for enum in contract.enums: + content += f".. py:class:: {enum.name}\n\n" + for value in enum.values: + content += f" .. py:attribute:: {value}\n\n" + if contract.structs: content += _insert_content_section("Structs") for struct in contract.structs: content += self._generate_struct_docs(struct) + if contract.events: + content += _insert_content_section("Events") + for event in contract.events: + content += f".. py:class:: {event.name}\n\n" + for field in event.fields: + content += f" .. py:attribute:: {field.name}\n\n" + content += f" {f'indexed({field.type})' if field.indexed else field.type}\n\n" + + if contract.constants: + content += _insert_content_section("Constants") + for constant in contract.constants: + content += f".. py:data:: {constant.name}\n\n" + content += f" {constant.type}: {constant.value}\n\n" + if contract.functions: content += _insert_content_section("Functions") for func in contract.functions: diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 92f5ddb..358c90e 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -13,12 +13,20 @@ valid_uints = {f"uint{8 * (i+1)}" for i in range(32)} VALID_VYPER_TYPES = {*valid_ints, *valid_uints, "address", "bool", "Bytes", "String"} +ENUM_PATTERN = r"enum\s+(\w+)\s*\{([^}]*)\}" CONSTANT_PATTERN = r"(\w+):\s*constant\((\w+)\)\s*=\s*(.*?)$" STRUCT_PATTERN = r"struct\s+(\w+)\s*{([^}]*)}" +EVENT_PATTERN = r"event\s+(\w+)\((.*?)\)" FUNCTION_PATTERN = r'@(external|internal)\s+def\s+([^(]+)\(([^)]*)\)(\s*->\s*[^:]+)?:\s*("""[\s\S]*?""")?' PARAM_PATTERN = r"(\w+:\s*DynArray\[[^\]]+\]|\w+:\s*\w+)" +@dataclass +class Enum: + name: str + values: List[str] + + @dataclass class Constant: @@ -92,6 +100,27 @@ class Struct: fields: List[Parameter] +@dataclass +class EventParameter: + """Vyper event parameter representation.""" + + name: str + type: str + indexed: bool + + def __post_init__(self) -> None: + if self.type not in VALID_VYPER_TYPES: + logger.warning(f"{self} is not a valid Vyper type") + + +@dataclass +class Event: + """Vyper event representation.""" + + name: str + params: List[EventParameter] + + @dataclass class Function: """Vyper function representation.""" @@ -116,7 +145,10 @@ class Contract: name: str path: str docstring: Optional[str] + enums: List[Enum] structs: List[Struct] + events: List[Event] + constants: List[Constant] functions: List[Function] @@ -147,21 +179,15 @@ def _parse_contract(self, file_path: str) -> Contract: name = os.path.basename(file_path).replace(".vy", "") rel_path = os.path.relpath(file_path, self.contracts_dir) - # Extract contract docstring - docstring = self._extract_contract_docstring(content) - - # Extract structs - structs = self._extract_structs(content) - - # Extract functions - functions = self._extract_functions(content) - return Contract( name=name, path=rel_path, - docstring=docstring, - structs=structs, - functions=functions, + docstring=self._extract_contract_docstring(content), + enums=self._extract_enums(content), + structs=self._extract_structs(content), + events=self._extract_events(content), + constants=self._extract_constants(content), + functions=self._extract_functions(content), ) def _extract_contract_docstring(self, content: str) -> Optional[str]: @@ -169,6 +195,16 @@ def _extract_contract_docstring(self, content: str) -> Optional[str]: match = re.search(r'^"""(.*?)"""', content, re.DOTALL | re.MULTILINE) return match.group(1).strip() if match else None + def _extract_enums(self, content: str) -> List[Enum]: + """Extract all enums from the contract.""" + return [ + Enum( + name=match.group(1).strip(), + values=self._parse_enum_values(match.group(2).strip()), + ) + for match in re.finditer(ENUM_PATTERN, content) + ] + def _extract_constants(self, content: str) -> List[Constant]: """Extract constants from the contract.""" return [ @@ -180,7 +216,8 @@ def _extract_constants(self, content: str) -> List[Constant]: for match in re.finditer(CONSTANT_PATTERN, content, re.MULTILINE) ] - def _extract_structs(self, content: str) -> List[Struct]: + @staticmethod + def _extract_structs(content: str) -> List[Struct]: """Extract all structs from the contract.""" return [ Struct( @@ -190,7 +227,19 @@ def _extract_structs(self, content: str) -> List[Struct]: for match in re.finditer(STRUCT_PATTERN, content) ] - def _extract_functions(self, content: str) -> List[Function]: + @staticmethod + def _extract_events(content: str) -> List[Event]: + """Extract all events from the contract.""" + return [ + Event( + name=match.group(1).strip(), + params=self._parse_event_params(match.group(2).strip()), + ) + for match in re.finditer(EVENT_PATTERN, content) + ] + + @staticmethod + def _extract_functions(content: str) -> List[Function]: """Extract all functions from the contract, with @external functions listed first.""" external_functions = [] internal_functions = [] @@ -234,3 +283,17 @@ def _parse_params(params_str: str) -> List[Parameter]: typ = Tuple(type_str[1:-1].split(",")) if type_str[1] == "(" else type_str params.append(Parameter(name=name.strip(), type=typ)) return params + + @staticmethod + def _parse_event_params(params_str: str) -> List[EventParameter]: + """Parse event parameters.""" + params = [] + for param_str in params_str.split("\n"): + name, type = param_str.strip().split(":") + type = type.strip() + if indexed := "indexed" in type: + assert type.startswith("indexed(") and type.endswith(")"), type + type = type[8:-1] + param = EventParameter(name=name.strip(), type=type, indexed=indexed) + params.append(param) + return params From 0e3e96ab19e8d38c5750cd2944967a8e28048a4e Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:21:53 +0000 Subject: [PATCH 20/39] feat: bifurcate internal and external functions --- sphinx_autodoc_vyper/generator.py | 11 ++++++++--- sphinx_autodoc_vyper/parser.py | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index fa0641e..ba097f4 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -97,9 +97,14 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: content += f".. py:data:: {constant.name}\n\n" content += f" {constant.type}: {constant.value}\n\n" - if contract.functions: - content += _insert_content_section("Functions") - for func in contract.functions: + if contract.external_functions: + content += _insert_content_section("External Functions") + for func in contract.external_functions: + content += self._generate_function_docs(func) + + if contract.internal_functions: + content += _insert_content_section("Internal Functions") + for func in contract.internal_functions: content += self._generate_function_docs(func) with open( diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 358c90e..0fb7e8f 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -179,6 +179,8 @@ def _parse_contract(self, file_path: str) -> Contract: name = os.path.basename(file_path).replace(".vy", "") rel_path = os.path.relpath(file_path, self.contracts_dir) + external_functions, internal_functions = self._extract_functions(content) + return Contract( name=name, path=rel_path, @@ -187,7 +189,8 @@ def _parse_contract(self, file_path: str) -> Contract: structs=self._extract_structs(content), events=self._extract_events(content), constants=self._extract_constants(content), - functions=self._extract_functions(content), + external_functions=external_functions, + internal_functions=internal_functions, ) def _extract_contract_docstring(self, content: str) -> Optional[str]: @@ -238,8 +241,12 @@ def _extract_events(content: str) -> List[Event]: for match in re.finditer(EVENT_PATTERN, content) ] - @staticmethod - def _extract_functions(content: str) -> List[Function]: + @classmethod + def _extract_functions( + self, + content: str, + internal: bool, + ) -> Tuple[List[Function], List[Function]]: """Extract all functions from the contract, with @external functions listed first.""" external_functions = [] internal_functions = [] @@ -253,10 +260,9 @@ def _extract_functions(content: str) -> List[Function]: ) docstring = match.group(5)[3:-3].strip() if match.group(5) else None - params = self._parse_params(params_str) function = Function( name=name, - params=params, + params=cls._parse_params(params_str), return_type=return_type, docstring=docstring, ) @@ -266,8 +272,7 @@ def _extract_functions(content: str) -> List[Function]: else: internal_functions.append(function) - # Combine external and internal functions, with external functions first - return external_functions + internal_functions + return external_functions, internal_functions @staticmethod def _parse_params(params_str: str) -> List[Parameter]: From 39c2359c36bfdace7fd9e73221ee64aef8d44e23 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:23:00 +0000 Subject: [PATCH 21/39] fix: broken import --- sphinx_autodoc_vyper/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 0fb7e8f..0caa314 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -3,6 +3,7 @@ import logging import os import re +import typing from dataclasses import dataclass from pathlib import Path from typing import Any, List, Optional, Union @@ -246,7 +247,7 @@ def _extract_functions( self, content: str, internal: bool, - ) -> Tuple[List[Function], List[Function]]: + ) -> typing.Tuple[List[Function], List[Function]]: """Extract all functions from the contract, with @external functions listed first.""" external_functions = [] internal_functions = [] From ec0a9b0995b1ac3049e816ed97ab1af8ad0feea5 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:24:48 +0000 Subject: [PATCH 22/39] chore: cleanup --- sphinx_autodoc_vyper/parser.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 0caa314..79afe6d 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -153,6 +153,9 @@ class Contract: functions: List[Function] +Functions = typing.Tuple[List[Function], List[Function]] + + class VyperParser: """Parser for Vyper smart contracts.""" @@ -243,11 +246,7 @@ def _extract_events(content: str) -> List[Event]: ] @classmethod - def _extract_functions( - self, - content: str, - internal: bool, - ) -> typing.Tuple[List[Function], List[Function]]: + def _extract_functions(cls, content: str) -> Functions: """Extract all functions from the contract, with @external functions listed first.""" external_functions = [] internal_functions = [] From cf9a44b397b6c26637080a8bf23df4f4b4659253 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:25:33 +0000 Subject: [PATCH 23/39] chore: cleanup --- sphinx_autodoc_vyper/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 79afe6d..32fbf68 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -150,7 +150,8 @@ class Contract: structs: List[Struct] events: List[Event] constants: List[Constant] - functions: List[Function] + external_functions: List[Function] + internal_functions: List[Function] Functions = typing.Tuple[List[Function], List[Function]] From d7b6b9da1c449e747374d3d26e37178d3e51bd19 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:35:18 +0000 Subject: [PATCH 24/39] fix: enum matcher --- sphinx_autodoc_vyper/parser.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 32fbf68..0f7f247 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -14,7 +14,7 @@ valid_uints = {f"uint{8 * (i+1)}" for i in range(32)} VALID_VYPER_TYPES = {*valid_ints, *valid_uints, "address", "bool", "Bytes", "String"} -ENUM_PATTERN = r"enum\s+(\w+)\s*\{([^}]*)\}" +ENUM_PATTERN = r'enum\s+(\w+)\s*:\s*([\w\s\n]+)' CONSTANT_PATTERN = r"(\w+):\s*constant\((\w+)\)\s*=\s*(.*?)$" STRUCT_PATTERN = r"struct\s+(\w+)\s*{([^}]*)}" EVENT_PATTERN = r"event\s+(\w+)\((.*?)\)" @@ -24,12 +24,15 @@ @dataclass class Enum: + """Vyper enum representation.""" + name: str values: List[str] @dataclass class Constant: + """Vyper constant representation.""" name: str type: str @@ -42,6 +45,8 @@ def __post_init__(self) -> None: @dataclass class Tuple: + """Vyper tuple representation.""" + types: List[str] def __post_init__(self) -> None: @@ -203,12 +208,13 @@ def _extract_contract_docstring(self, content: str) -> Optional[str]: match = re.search(r'^"""(.*?)"""', content, re.DOTALL | re.MULTILINE) return match.group(1).strip() if match else None - def _extract_enums(self, content: str) -> List[Enum]: + @staticmethod + def _extract_enums(content: str) -> List[Enum]: """Extract all enums from the contract.""" return [ Enum( name=match.group(1).strip(), - values=self._parse_enum_values(match.group(2).strip()), + values=[value.strip() for value in match.group(2).split("\n") if value.strip()], ) for match in re.finditer(ENUM_PATTERN, content) ] From dfefb55b0e8bacd081f0a369f10b849d16b51ff5 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:45:14 +0000 Subject: [PATCH 25/39] feat: document variables --- sphinx_autodoc_vyper/generator.py | 6 ++++++ sphinx_autodoc_vyper/parser.py | 36 +++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index ba097f4..a1a3434 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -97,6 +97,12 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: content += f".. py:data:: {constant.name}\n\n" content += f" {constant.type}: {constant.value}\n\n" + if contract.variables: + content += _insert_content_section("Variables") + for variable in contract.variables: + content += f".. py:attribute:: {variable.name}\n\n" + content += f" {f'public({variable.type})' if variable.visibility == 'public' else variable.type}" + if contract.external_functions: content += _insert_content_section("External Functions") for func in contract.external_functions: diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 0f7f247..28e4387 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -14,8 +14,9 @@ valid_uints = {f"uint{8 * (i+1)}" for i in range(32)} VALID_VYPER_TYPES = {*valid_ints, *valid_uints, "address", "bool", "Bytes", "String"} -ENUM_PATTERN = r'enum\s+(\w+)\s*:\s*([\w\s\n]+)' +ENUM_PATTERN = r"enum\s+(\w+)\s*:\s*([\w\s\n]+)" CONSTANT_PATTERN = r"(\w+):\s*constant\((\w+)\)\s*=\s*(.*?)$" +VARIABLE_PATTERN = r"(\w+):\s*(\w*)\s*(\w+)\s*" STRUCT_PATTERN = r"struct\s+(\w+)\s*{([^}]*)}" EVENT_PATTERN = r"event\s+(\w+)\((.*?)\)" FUNCTION_PATTERN = r'@(external|internal)\s+def\s+([^(]+)\(([^)]*)\)(\s*->\s*[^:]+)?:\s*("""[\s\S]*?""")?' @@ -43,6 +44,19 @@ def __post_init__(self) -> None: logger.warning(f"{self} is not a valid Vyper type") +@dataclass +class Variable: + """Vyper variable representation.""" + + name: str + type: str + visibility: str + + def __post_init__(self) -> None: + if self.type not in VALID_VYPER_TYPES: + logger.warning(f"{self} is not a valid Vyper type") + + @dataclass class Tuple: """Vyper tuple representation.""" @@ -155,6 +169,7 @@ class Contract: structs: List[Struct] events: List[Event] constants: List[Constant] + variables: List[Variable] external_functions: List[Function] internal_functions: List[Function] @@ -199,6 +214,7 @@ def _parse_contract(self, file_path: str) -> Contract: structs=self._extract_structs(content), events=self._extract_events(content), constants=self._extract_constants(content), + variables=self._extract_variables(content), external_functions=external_functions, internal_functions=internal_functions, ) @@ -214,7 +230,11 @@ def _extract_enums(content: str) -> List[Enum]: return [ Enum( name=match.group(1).strip(), - values=[value.strip() for value in match.group(2).split("\n") if value.strip()], + values=[ + value.strip() + for value in match.group(2).split("\n") + if value.strip() + ], ) for match in re.finditer(ENUM_PATTERN, content) ] @@ -230,6 +250,18 @@ def _extract_constants(self, content: str) -> List[Constant]: for match in re.finditer(CONSTANT_PATTERN, content, re.MULTILINE) ] + @staticmethod + def _extract_variables(content: str) -> List[Variable]: + """Extract all variables from the contract.""" + return [ + Variable( + name=match.group(1).strip(), + type=match.group(3).strip(), + visibility=match.group(2).strip() if match.group(2) else "private", + ) + for match in re.finditer(VARIABLE_PATTERN, content) + ] + @staticmethod def _extract_structs(content: str) -> List[Struct]: """Extract all structs from the contract.""" From 19fb3a273b4c3067908b2676ccbe739c4d1eb165 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:54:18 +0000 Subject: [PATCH 26/39] fix: variable parsing --- sphinx_autodoc_vyper/parser.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 28e4387..c9ccd57 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -16,7 +16,7 @@ ENUM_PATTERN = r"enum\s+(\w+)\s*:\s*([\w\s\n]+)" CONSTANT_PATTERN = r"(\w+):\s*constant\((\w+)\)\s*=\s*(.*?)$" -VARIABLE_PATTERN = r"(\w+):\s*(\w*)\s*(\w+)\s*" +VARIABLE_PATTERN = r"(\w+):\s*(public\((\w+)\)|(\w+))" STRUCT_PATTERN = r"struct\s+(\w+)\s*{([^}]*)}" EVENT_PATTERN = r"event\s+(\w+)\((.*?)\)" FUNCTION_PATTERN = r'@(external|internal)\s+def\s+([^(]+)\(([^)]*)\)(\s*->\s*[^:]+)?:\s*("""[\s\S]*?""")?' @@ -256,8 +256,10 @@ def _extract_variables(content: str) -> List[Variable]: return [ Variable( name=match.group(1).strip(), - type=match.group(3).strip(), - visibility=match.group(2).strip() if match.group(2) else "private", + type=match.group(3).strip() + if match.group(3) + else match.group(4).strip(), + visibility="public" if match.group(2) else "private", ) for match in re.finditer(VARIABLE_PATTERN, content) ] From 450b5ce868f7d6119ffddb83de8fa554ee2c98de Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:58:44 +0000 Subject: [PATCH 27/39] fix: variable parsing --- sphinx_autodoc_vyper/parser.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index c9ccd57..e45db2b 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -252,7 +252,27 @@ def _extract_constants(self, content: str) -> List[Constant]: @staticmethod def _extract_variables(content: str) -> List[Variable]: - """Extract all variables from the contract.""" + """Extract all contract-level variables from the contract.""" + # Split the content into lines and filter out lines that are likely inside functions + lines = content.splitlines() + contract_level_lines = [] + inside_function = False + + for line in lines: + stripped_line = line.strip() + # Detect function definitions to avoid parsing their contents + if stripped_line.startswith("@") or stripped_line.startswith("def "): + inside_function = True + elif stripped_line == "": + inside_function = False + + # Only consider lines outside of functions + if not inside_function and stripped_line: + contract_level_lines.append(stripped_line) + + # Join the filtered lines back into a single string for regex processing + filtered_content = "\n".join(contract_level_lines) + return [ Variable( name=match.group(1).strip(), @@ -261,7 +281,7 @@ def _extract_variables(content: str) -> List[Variable]: else match.group(4).strip(), visibility="public" if match.group(2) else "private", ) - for match in re.finditer(VARIABLE_PATTERN, content) + for match in re.finditer(VARIABLE_PATTERN, filtered_content) ] @staticmethod From 0c7a6a6b93a1c1639105488f02cf15ab12179896 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 22:59:55 +0000 Subject: [PATCH 28/39] fix: temp disable variable docs --- sphinx_autodoc_vyper/generator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index a1a3434..4906854 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -97,11 +97,12 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: content += f".. py:data:: {constant.name}\n\n" content += f" {constant.type}: {constant.value}\n\n" - if contract.variables: - content += _insert_content_section("Variables") - for variable in contract.variables: - content += f".. py:attribute:: {variable.name}\n\n" - content += f" {f'public({variable.type})' if variable.visibility == 'public' else variable.type}" + # TODO: fix this + #if contract.variables: + # content += _insert_content_section("Variables") + # for variable in contract.variables: + # content += f".. py:attribute:: {variable.name}\n\n" + # content += f" {f'public({variable.type})' if variable.visibility == 'public' else variable.type}" if contract.external_functions: content += _insert_content_section("External Functions") From 4dac5a523ab00c656ef379d56bbe2f1444f6047f Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:04:47 +0000 Subject: [PATCH 29/39] feat(test): test new parsers --- sphinx_autodoc_vyper/generator.py | 2 +- tests/test_parser.py | 114 +++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 12 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index 4906854..a8ce9c4 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -98,7 +98,7 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: content += f" {constant.type}: {constant.value}\n\n" # TODO: fix this - #if contract.variables: + # if contract.variables: # content += _insert_content_section("Variables") # for variable in contract.variables: # content += f".. py:attribute:: {variable.name}\n\n" diff --git a/tests/test_parser.py b/tests/test_parser.py index 8a6252f..8632051 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -104,6 +104,50 @@ def foo() -> bool: assert docstring == "This is a contract docstring." +def test_extract_enums() -> None: + """Test enum extraction from contract.""" + content = """ + enum Status: + PENDING + COMPLETED + """ + parser = VyperParser(Path(".")) + enums = parser._extract_enums(content) + assert len(enums) == 1 + assert enums[0].name == "Status" + assert enums[0].values == ["PENDING", "COMPLETED"] + + +def test_extract_constants() -> None: + """Test constant extraction from contract.""" + content = """ + MAX_SUPPLY: constant(uint256) = 1000000 + """ + parser = VyperParser(Path(".")) + constants = parser._extract_constants(content) + assert len(constants) == 1 + assert constants[0].name == "MAX_SUPPLY" + assert constants[0].type == "uint256" + assert constants[0].value == "1000000" + + +def test_extract_variables() -> None: + """Test variable extraction from contract.""" + content = """ + owner: public(address) + balance: uint256 + """ + parser = VyperParser(Path(".")) + variables = parser._extract_variables(content) + assert len(variables) == 2 + assert variables[0].name == "owner" + assert variables[0].type == "address" + assert variables[0].visibility == "public" + assert variables[1].name == "balance" + assert variables[1].type == "uint256" + assert variables[1].visibility == "private" + + def test_extract_structs() -> None: """Test struct extraction from contract.""" content = """ @@ -123,8 +167,38 @@ def test_extract_structs() -> None: assert structs[0].fields[1].type == "address" -def test_parse_contract_with_functions_and_structs() -> None: - """Test parsing a contract with both functions and structs.""" +def test_extract_events() -> None: + """Test event extraction from contract.""" + content = """ + event Transfer: + from: address + to: address + value: uint256 + """ + parser = VyperParser(Path(".")) + events = parser._extract_events(content) + assert len(events) == 1 + assert events[0].name == "Transfer" + assert len(events[0].params) == 3 + assert events[0].params[0].name == "from" + assert events[0].params[0].type == "address" + assert events[0].params[1].name == "to" + assert events[0].params[1].type == "address" + assert events[0].params[2].name == "value" + assert events[0].params[2].type == "uint256" + + +def test_tuple_length() -> None: + """Test the length of a Tuple instance.""" + tuple_instance = Tuple(types=["uint256", "address", "bool"]) + assert len(tuple_instance) == 3 + + empty_tuple_instance = Tuple(types=[]) + assert len(empty_tuple_instance) == 0 + + +def test_parse_contract_with_functions_structs_enums_and_events() -> None: + """Test parsing a contract with functions, structs, enums, and events.""" contract_content = ''' """ This is a contract docstring. @@ -134,6 +208,15 @@ def test_parse_contract_with_functions_and_structs() -> None: field1: uint256 field2: address + enum Status: + PENDING + COMPLETED + + event Transfer: + from: address + to: address + value: uint256 + @external def transfer(to: address, amount: uint256) -> bool: """ @@ -155,6 +238,8 @@ def balance_of(owner: address) -> uint256: parser._extract_contract_docstring = lambda x: parser._extract_contract_docstring(contract_content) # type: ignore [method-assign,assignment] parser._extract_structs = lambda x: parser._extract_structs(contract_content) # type: ignore [method-assign,assignment] parser._extract_functions = lambda x: parser._extract_functions(contract_content) # type: ignore [method-assign,assignment] + parser._extract_enums = lambda x: parser._extract_enums(contract_content) # type: ignore [method-assign,assignment] + parser._extract_events = lambda x: parser._extract_events(contract_content) # type: ignore [method-assign,assignment] contract = parser.parse_contracts()[0] @@ -167,6 +252,22 @@ def balance_of(owner: address) -> uint256: assert contract.structs[0].fields[1].name == "field2" assert contract.structs[0].fields[1].type == "address" + # Assertions for enums + assert len(contract.enums) == 1 + assert contract.enums[0].name == "Status" + assert contract.enums[0].values == ["PENDING", "COMPLETED"] + + # Assertions for events + assert len(contract.events) == 1 + assert contract.events[0].name == "Transfer" + assert len(contract.events[0].params) == 3 + assert contract.events[0].params[0].name == "from" + assert contract.events[0].params[0].type == "address" + assert contract.events[0].params[1].name == "to" + assert contract.events[0].params[1].type == "address" + assert contract.events[0].params[2].name == "value" + assert contract.events[0].params[2].type == "uint256" + # Assertions for functions assert len(contract.functions) == 2 assert contract.functions[0].name == "transfer" @@ -182,12 +283,3 @@ def balance_of(owner: address) -> uint256: assert contract.functions[1].params[0].name == "owner" assert contract.functions[1].params[0].type == "address" assert contract.functions[1].return_type == "uint256" - - -def test_tuple_length() -> None: - """Test the length of a Tuple instance.""" - tuple_instance = Tuple(types=["uint256", "address", "bool"]) - assert len(tuple_instance) == 3 - - empty_tuple_instance = Tuple(types=[]) - assert len(empty_tuple_instance) == 0 From 1a16c40526aa95c4eb7451b9c5c5811771f1fdaf Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:10:12 +0000 Subject: [PATCH 30/39] feat(test): test docs generationn for new objs --- tests/test_generator.py | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_generator.py b/tests/test_generator.py index d0e4ce8..4339c40 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -81,6 +81,32 @@ def test_contract_rst_generation(contracts_dir: Path, output_dir: Path) -> None: assert "Get the token balance of an account" in content +def test_generate_enum_doc() -> None: + """Test documentation generation for Enum.""" + enum = Enum(name="Status", values=["PENDING", "COMPLETED"]) + generated_doc = generate_enum_doc(enum) + expected_doc = ".. py:enum:: Status\n :members:\n\n PENDING\n COMPLETED\n" + assert generated_doc == expected_doc + + +def test_generate_constant_doc() -> None: + """Test documentation generation for Constant.""" + constant = Constant(name="MAX_SUPPLY", type="uint256", value="1000000") + generated_doc = generate_constant_doc(constant) + expected_doc = ".. py:data:: MAX_SUPPLY\n\n :type: uint256\n :value: 1000000\n" + assert generated_doc == expected_doc + + +def test_generate_variable_doc() -> None: + """Test documentation generation for Variable.""" + variable = Variable(name="owner", type="address", visibility="public") + generated_doc = generate_variable_doc(variable) + expected_doc = ( + ".. py:attribute:: owner\n\n :type: address\n :visibility: public\n" + ) + assert generated_doc == expected_doc + + def test_generate_struct_docs() -> None: """Test struct documentation generation.""" struct = Struct( @@ -100,6 +126,26 @@ def test_generate_struct_docs() -> None: assert SphinxGenerator._generate_struct_docs(struct) == expected_output +def test_generate_event_doc() -> None: + """Test documentation generation for Event.""" + event = Event( + name="Transfer", + params=[ + Parameter(name="from", type="address"), + Parameter(name="to", type="address"), + Parameter(name="value", type="uint256"), + ], + ) + generated_doc = generate_event_doc(event) + expected_doc = ( + ".. py:event:: Transfer\n\n" + " :param address from: \n" + " :param address to: \n" + " :param uint256 value: \n" + ) + assert generated_doc == expected_doc + + def test_generate_docs_for_contract_with_functions_and_structs() -> None: """Test documentation generation for a contract with both functions and structs.""" contract = Contract( From f6ada670791760f7c9cccd6fe7575327c9762450 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:15:25 +0000 Subject: [PATCH 31/39] fix(test): various errs --- tests/test_generator.py | 6 +++++- tests/test_parser.py | 38 ++++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/tests/test_generator.py b/tests/test_generator.py index 4339c40..867f251 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -4,10 +4,14 @@ from sphinx_autodoc_vyper.generator import SphinxGenerator from sphinx_autodoc_vyper.parser import ( + Constant, Contract, + Enum, + Event, Function, Parameter, Struct, + Variable, VyperParser, ) @@ -161,7 +165,7 @@ def test_generate_docs_for_contract_with_functions_and_structs() -> None: ], ) ], - functions=[ + external_functions=[ Function( name="transfer", params=[ diff --git a/tests/test_parser.py b/tests/test_parser.py index 8632051..3586af1 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -33,9 +33,11 @@ def test_contract_parsing(contracts_dir: Path) -> None: assert "ERC20 Token Implementation" in contract.docstring # Test functions - assert len(contract.functions) == 2 - transfer_func = next(f for f in contract.functions if f.name == "transfer") - balance_func = next(f for f in contract.functions if f.name == "balance_of") + assert len(contract.external_functions) == 2 + transfer_func = next(f for f in contract.external_functions if f.name == "transfer") + balance_func = next( + f for f in contract.external_functions if f.name == "balance_of" + ) # Test transfer function assert isinstance(transfer_func, Function) @@ -57,7 +59,7 @@ def test_parameter_parsing(contracts_dir: Path) -> None: parser = VyperParser(contracts_dir) contracts = parser.parse_contracts() contract = next(c for c in contracts if c.name == "token") - transfer_func = next(f for f in contract.functions if f.name == "transfer") + transfer_func = next(f for f in contract.external_functions if f.name == "transfer") # Test parameters assert len(transfer_func.params) == 2 @@ -269,17 +271,17 @@ def balance_of(owner: address) -> uint256: assert contract.events[0].params[2].type == "uint256" # Assertions for functions - assert len(contract.functions) == 2 - assert contract.functions[0].name == "transfer" - assert len(contract.functions[0].params) == 2 - assert contract.functions[0].params[0].name == "to" - assert contract.functions[0].params[0].type == "address" - assert contract.functions[0].params[1].name == "amount" - assert contract.functions[0].params[1].type == "uint256" - assert contract.functions[0].return_type == "bool" - - assert contract.functions[1].name == "balance_of" - assert len(contract.functions[1].params) == 1 - assert contract.functions[1].params[0].name == "owner" - assert contract.functions[1].params[0].type == "address" - assert contract.functions[1].return_type == "uint256" + assert len(contract.external_functions) == 2 + assert contract.external_functions[0].name == "transfer" + assert len(contract.external_functions[0].params) == 2 + assert contract.external_functions[0].params[0].name == "to" + assert contract.external_functions[0].params[0].type == "address" + assert contract.external_functions[0].params[1].name == "amount" + assert contract.external_functions[0].params[1].type == "uint256" + assert contract.external_functions[0].return_type == "bool" + + assert contract.external_functions[1].name == "balance_of" + assert len(contract.external_functions[1].params) == 1 + assert contract.external_functions[1].params[0].name == "owner" + assert contract.external_functions[1].params[0].type == "address" + assert contract.external_functions[1].return_type == "uint256" From 6a9941af27f53dc155c83850619c133702091711 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:22:53 +0000 Subject: [PATCH 32/39] feat(test): test indexed event param parsing --- tests/test_parser.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 3586af1..d381a79 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -176,18 +176,37 @@ def test_extract_events() -> None: from: address to: address value: uint256 + event Indexed: + from: indexed(address) + to: indexed(address) + value: uint256 """ parser = VyperParser(Path(".")) events = parser._extract_events(content) - assert len(events) == 1 + assert len(events) == 2 assert events[0].name == "Transfer" assert len(events[0].params) == 3 assert events[0].params[0].name == "from" assert events[0].params[0].type == "address" + assert not events[0].params[0].indexed assert events[0].params[1].name == "to" assert events[0].params[1].type == "address" + assert not events[0].params[1].indexed assert events[0].params[2].name == "value" assert events[0].params[2].type == "uint256" + assert not events[0].params[2].indexed + + assert events[1].name == "Indexed" + assert len(events[1].params) == 3 + assert events[1].params[0].name == "from" + assert events[1].params[0].type == "address" + assert events[1].params[0].indexed + assert events[1].params[1].name == "to" + assert events[1].params[1].type == "address" + assert events[1].params[1].indexed + assert events[1].params[2].name == "value" + assert events[1].params[2].type == "uint256" + assert events[1].params[2].indexed def test_tuple_length() -> None: From 36c57741c530f2f25db7047607a8131d748d76cb Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:26:13 +0000 Subject: [PATCH 33/39] fix: extract structs from source --- sphinx_autodoc_vyper/parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index e45db2b..aa8ce86 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -284,13 +284,13 @@ def _extract_variables(content: str) -> List[Variable]: for match in re.finditer(VARIABLE_PATTERN, filtered_content) ] - @staticmethod - def _extract_structs(content: str) -> List[Struct]: + @classmethod + def _extract_structs(cls, content: str) -> List[Struct]: """Extract all structs from the contract.""" return [ Struct( name=match.group(1).strip(), - fields=self._parse_params(match.group(2).strip()), + fields=cls._parse_params(match.group(2).strip()), ) for match in re.finditer(STRUCT_PATTERN, content) ] From 0a575900792ddaddef2849f65b8ce3750491a0ef Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:34:01 +0000 Subject: [PATCH 34/39] fix(test): fix tests --- sphinx_autodoc_vyper/generator.py | 33 ++++++++++++++++++++++--------- tests/test_generator.py | 7 ++++++- tests/test_parser.py | 4 ++-- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index a8ce9c4..77925c4 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -74,9 +74,7 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: if contract.enums: content += _insert_content_section("Enums") for enum in contract.enums: - content += f".. py:class:: {enum.name}\n\n" - for value in enum.values: - content += f" .. py:attribute:: {value}\n\n" + content += generate_enum_doc(enum) if contract.structs: content += _insert_content_section("Structs") @@ -86,16 +84,12 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: if contract.events: content += _insert_content_section("Events") for event in contract.events: - content += f".. py:class:: {event.name}\n\n" - for field in event.fields: - content += f" .. py:attribute:: {field.name}\n\n" - content += f" {f'indexed({field.type})' if field.indexed else field.type}\n\n" + content += generate_event_doc(event) if contract.constants: content += _insert_content_section("Constants") for constant in contract.constants: - content += f".. py:data:: {constant.name}\n\n" - content += f" {constant.type}: {constant.value}\n\n" + content += generate_constant_doc(constant) # TODO: fix this # if contract.variables: @@ -143,3 +137,24 @@ def _generate_struct_docs(struct: Struct) -> str: def _insert_content_section(name: str) -> str: """Insert a hyperlinked content section accessible from the docs index.""" return f"{name}\n{'-' * len(name)}\n\n" + + +def generate_enum_doc(enum: Enum) -> str: + content = f".. py:class:: {enum.name}\n\n" + for value in enum.values: + content += f" .. py:attribute:: {value}\n\n" + return content + + +def generate_constant_doc(constant: Constant) -> str: + return f".. py:data:: {constant.name}\n\n {constant.type}: {constant.value}\n\n" + + +def generate_event_doc(event: Event) -> str: + content = f".. py:class:: {event.name}\n\n" + for field in event.fields: + content += f" .. py:attribute:: {field.name}\n\n" + content += ( + f" {f'indexed({field.type})' if field.indexed else field.type}\n\n" + ) + return content diff --git a/tests/test_generator.py b/tests/test_generator.py index 867f251..613b7f7 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -2,7 +2,12 @@ from pathlib import Path -from sphinx_autodoc_vyper.generator import SphinxGenerator +from sphinx_autodoc_vyper.generator import ( + SphinxGenerator, + generate_constant_doc, + generate_enum_doc, + generate_event_doc, +) from sphinx_autodoc_vyper.parser import ( Constant, Contract, diff --git a/tests/test_parser.py b/tests/test_parser.py index d381a79..ffb6d61 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -89,7 +89,7 @@ def test_empty_contract(tmp_path: Path) -> None: assert len(contracts) == 1 assert contracts[0].name == "empty" assert contracts[0].docstring is None - assert len(contracts[0].functions) == 0 + assert len(contracts[0].external_functions) == 0 def test_extract_contract_docstring() -> None: @@ -195,7 +195,7 @@ def test_extract_events() -> None: assert events[0].params[2].name == "value" assert events[0].params[2].type == "uint256" assert not events[0].params[2].indexed - + assert events[1].name == "Indexed" assert len(events[1].params) == 3 assert events[1].params[0].name == "from" From bce681ee769a61068ddb0a8c83fb84dbe7ef2383 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:35:44 +0000 Subject: [PATCH 35/39] fix: missing imports --- sphinx_autodoc_vyper/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index 77925c4..c305f54 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -3,7 +3,7 @@ import os from typing import List -from .parser import Contract, Function, Struct +from .parser import Constant, Contract, Enum, Event, Function, Struct INDEX_RST = """Vyper Smart Contracts Documentation ================================ From f5960411916bc808e1f9bf48d9c95fb31da119fd Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:43:43 +0000 Subject: [PATCH 36/39] chore: refactor --- sphinx_autodoc_vyper/generator.py | 29 ++++------------------------- sphinx_autodoc_vyper/parser.py | 17 +++++++++++++++++ tests/test_generator.py | 16 ++++------------ 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/sphinx_autodoc_vyper/generator.py b/sphinx_autodoc_vyper/generator.py index c305f54..760abf5 100644 --- a/sphinx_autodoc_vyper/generator.py +++ b/sphinx_autodoc_vyper/generator.py @@ -3,7 +3,7 @@ import os from typing import List -from .parser import Constant, Contract, Enum, Event, Function, Struct +from .parser import Contract, Function, Struct INDEX_RST = """Vyper Smart Contracts Documentation ================================ @@ -74,7 +74,7 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: if contract.enums: content += _insert_content_section("Enums") for enum in contract.enums: - content += generate_enum_doc(enum) + content += enum.generate_docs() if contract.structs: content += _insert_content_section("Structs") @@ -84,12 +84,12 @@ def _generate_contract_docs(self, contracts: List[Contract]) -> None: if contract.events: content += _insert_content_section("Events") for event in contract.events: - content += generate_event_doc(event) + content += event.generate_docs() if contract.constants: content += _insert_content_section("Constants") for constant in contract.constants: - content += generate_constant_doc(constant) + content += constant.generate_docs() # TODO: fix this # if contract.variables: @@ -137,24 +137,3 @@ def _generate_struct_docs(struct: Struct) -> str: def _insert_content_section(name: str) -> str: """Insert a hyperlinked content section accessible from the docs index.""" return f"{name}\n{'-' * len(name)}\n\n" - - -def generate_enum_doc(enum: Enum) -> str: - content = f".. py:class:: {enum.name}\n\n" - for value in enum.values: - content += f" .. py:attribute:: {value}\n\n" - return content - - -def generate_constant_doc(constant: Constant) -> str: - return f".. py:data:: {constant.name}\n\n {constant.type}: {constant.value}\n\n" - - -def generate_event_doc(event: Event) -> str: - content = f".. py:class:: {event.name}\n\n" - for field in event.fields: - content += f" .. py:attribute:: {field.name}\n\n" - content += ( - f" {f'indexed({field.type})' if field.indexed else field.type}\n\n" - ) - return content diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index aa8ce86..955a2a7 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -30,6 +30,12 @@ class Enum: name: str values: List[str] + def generate_docs(self) -> str: + content = f".. py:class:: {self.name}\n\n" + for value in self.values: + content += f" .. py:attribute:: {value}\n\n" + return content + @dataclass class Constant: @@ -43,6 +49,9 @@ def __post_init__(self) -> None: if self.type is not None and self.type not in VALID_VYPER_TYPES: logger.warning(f"{self} is not a valid Vyper type") + def generate_docs(self) -> str: + return f".. py:data:: {self.name}\n\n {self.type}: {self.value}\n\n" + @dataclass class Variable: @@ -140,6 +149,14 @@ class Event: name: str params: List[EventParameter] + def generate_docs(self) -> str: + content = f".. py:class:: {event.name}\n\n" + for field in event.fields: + type_str = f"indexed({field.type})" if field.indexed else field.type + content += f" .. py:attribute:: {field.name}\n\n" + content += f" {type_str}\n\n" + return content + @dataclass class Function: diff --git a/tests/test_generator.py b/tests/test_generator.py index 613b7f7..26edb65 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -2,12 +2,7 @@ from pathlib import Path -from sphinx_autodoc_vyper.generator import ( - SphinxGenerator, - generate_constant_doc, - generate_enum_doc, - generate_event_doc, -) +from sphinx_autodoc_vyper.generator import SphinxGenerator from sphinx_autodoc_vyper.parser import ( Constant, Contract, @@ -93,17 +88,15 @@ def test_contract_rst_generation(contracts_dir: Path, output_dir: Path) -> None: def test_generate_enum_doc() -> None: """Test documentation generation for Enum.""" enum = Enum(name="Status", values=["PENDING", "COMPLETED"]) - generated_doc = generate_enum_doc(enum) expected_doc = ".. py:enum:: Status\n :members:\n\n PENDING\n COMPLETED\n" - assert generated_doc == expected_doc + assert enum.generate_docs() == expected_doc def test_generate_constant_doc() -> None: """Test documentation generation for Constant.""" constant = Constant(name="MAX_SUPPLY", type="uint256", value="1000000") - generated_doc = generate_constant_doc(constant) expected_doc = ".. py:data:: MAX_SUPPLY\n\n :type: uint256\n :value: 1000000\n" - assert generated_doc == expected_doc + assert constant.generate_docs() == expected_doc def test_generate_variable_doc() -> None: @@ -145,14 +138,13 @@ def test_generate_event_doc() -> None: Parameter(name="value", type="uint256"), ], ) - generated_doc = generate_event_doc(event) expected_doc = ( ".. py:event:: Transfer\n\n" " :param address from: \n" " :param address to: \n" " :param uint256 value: \n" ) - assert generated_doc == expected_doc + assert event.generate_docs() == expected_doc def test_generate_docs_for_contract_with_functions_and_structs() -> None: From 2caa7ba280af50964274ec8db65253d27dec79d6 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:45:50 +0000 Subject: [PATCH 37/39] fix: name err --- sphinx_autodoc_vyper/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index 955a2a7..b17e38b 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -150,8 +150,8 @@ class Event: params: List[EventParameter] def generate_docs(self) -> str: - content = f".. py:class:: {event.name}\n\n" - for field in event.fields: + content = f".. py:class:: {self.name}\n\n" + for field in self.fields: type_str = f"indexed({field.type})" if field.indexed else field.type content += f" .. py:attribute:: {field.name}\n\n" content += f" {type_str}\n\n" From 72b2a9d1e5e9bead27e6e291bc5885a3cd3b12ae Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:47:36 +0000 Subject: [PATCH 38/39] feat(test): increase test verbosity --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de35c74..1c13348 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,5 +74,5 @@ ignore_missing_imports = true [tool.pytest.ini_options] minversion = "7.0" -addopts = "-ra -q --cov=sphinx_autodoc_vyper" +addopts = "-ra -q --cov=sphinx_autodoc_vyper -vv" testpaths = ["tests"] \ No newline at end of file From 620b22bb7bf509969f0ebe9b9188c85055218f08 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sat, 2 Nov 2024 23:53:29 +0000 Subject: [PATCH 39/39] fix: name err --- sphinx_autodoc_vyper/parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx_autodoc_vyper/parser.py b/sphinx_autodoc_vyper/parser.py index b17e38b..07f8752 100644 --- a/sphinx_autodoc_vyper/parser.py +++ b/sphinx_autodoc_vyper/parser.py @@ -151,9 +151,9 @@ class Event: def generate_docs(self) -> str: content = f".. py:class:: {self.name}\n\n" - for field in self.fields: - type_str = f"indexed({field.type})" if field.indexed else field.type - content += f" .. py:attribute:: {field.name}\n\n" + for param in self.params: + type_str = f"indexed({param.type})" if param.indexed else param.type + content += f" .. py:attribute:: {param.name}\n\n" content += f" {type_str}\n\n" return content