lucsorel / py2puml Goto Github PK
View Code? Open in Web Editor NEWGenerate PlantUML class diagrams to document your Python application.
Home Page: https://pypi.org/project/py2puml/
License: MIT License
Generate PlantUML class diagrams to document your Python application.
Home Page: https://pypi.org/project/py2puml/
License: MIT License
Using PlantUML namespaces enables:
Py2puml currently outputs this diagram:
Whereas the nested namespaces could be output, improving the understanding of the codebase organization:
When an attribute is assigned more than once in class constructor
class DupAttr:
def __init__(self, name=None):
self.name = name
if not self.name:
self.name = ''
The attribute(here "name") appears several times.
@startuml
class umlclass.DupAttr {
name: None
name: None
}
@enduml
#3636 Arrows from/to class fields
Right now, PlantUML would describe the composition relationship between a person and its address like this (an arrow between the 2 classes):
@startuml
class Address {
street: str
zipcode: str
city: str
}
class Person {
name: str
address: Address
}
Person -> Address
@enduml
But it would be interesting to have the relationship starting from the Person.address
field:
@startuml
class Address {
street: str
zipcode: str
city: str
}
class Person {
name: str
address: Address
}
Person::address -> Address
@enduml
Although the rendering is not so relevant on this example.
Caution:
Hi @lucsorel, given the following example where the class's attribute can have different values depending on an argument, the uml output shows two copies of the same attribute (hands). Not really a big deal since I rarely have if else statements in my constructor but would be good to know if it can be avoided.
class Human(Mammals, BiPedal):
def __init__(self, long_hands: bool):
Mammals.__init__(self)
BiPedal.__init__(self)
if long_hands:
self.hands: Hands = Hands(length=4)
else:
self.hands: Hands = Hands(length=2)
def eat(self) -> None:
pass
def move(self) -> None:
pass
py2puml uses internally a mix of code inspection and Abstract Syntax Tree parsing. The whole code based should be refactored to use AST parsing only as it is more robust and straightforward.
using the defaults attribute of the constructor method should help
getattr(definition_type.__init__, '__defaults__', None)
Dear Developers,
I have encountered the following error when I used py2puml.
I was trying to draw uml diagram of the deepmd package with the command,
py2puml deepmd/train deepmd.train
I have the error messages:
Traceback (most recent call last):
File "/data/ybzhuang/conda/env/dpff/bin/py2puml", line 8, in <module>
sys.exit(run())
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/cli.py", line 17, in run
print(''.join(py2puml(args.path, args.module)))
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/py2puml.py", line 12, in py2puml
inspect_package(
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/inspection/inspectpackage.py", line 20, in inspect_package
inspect_module(
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/inspection/inspectmodule.py", line 60, in inspect_module
inspect_domain_definition(definition_type, root_module_name, domain_items_by_fqn, domain_relations)
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/inspection/inspectmodule.py", line 47, in inspect_domain_definition
inspect_class_type(
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/inspection/inspectclass.py", line 99, in inspect_class_type
attributes = inspect_static_attributes(
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/inspection/inspectclass.py", line 78, in inspect_static_attributes
attr_type, full_namespaced_definitions = shorten_compound_type_annotation(attr_raw_type, module_resolver)
File "/data/ybzhuang/conda/env/dpff/lib/python3.9/site-packages/py2puml/parsing/astvisitors.py", line 215, in shorten_compound_type_annotation
raise ValueError(f'Could not resolve type {compound_type_part} in module {module_resolver.module}: it needs to be imported explicitely.')
ValueError: Could not resolve type deepmd.train.run_options.HVD in module <module 'deepmd.train.run_options' from '/data/ybzhuang/repositories/deepmd-kit/deepmd/train/run_options.py'>: it needs to be imported explicitely.
How should I bypass this implicit-typing errors?
using inspect.getclasstree or getmro should help
from inspect import getclasstree
getclasstree([definition_type.__base__])
First: awesome package, thx a lot ^^
Second: am I not finding the option, or is is currently not possible to extract the methods of classes?
Best regards
mskoenz
Hi - interesting project. UML is currently underestimate, IMO it gives great overviews.
I just wanted to point out to dataclass transforms - I believe it would allow to treat Pydantic, attrs, or Django models like dataclasses with little additional coding.
I installed with python -m pip install py2puml
I run py2puml . . in a directory containing my python files.
I get the following error:
TypeError: the 'package' argument is required to perform a relative import for '..bk'
Tried many things but cannot get it to work.
Made these changes to mark abstract classes as such in the diagram #27
I have examples files that are stored according to this hierarchy
test
__init__.py
test.py
I struggled to run the .exe as I would get either ModuleNotFoundError: No module named 'test.test'
or an empty file depending on the arguments provided:
py2puml.exe test\test test
@startuml test
!pragma useIntermediatePackages false
footer Generated by //py2puml//
@enduml
it turned out that py2puml.exe test test
doesn't work as expected but python -m py2puml test test
does.
Would it be possible for you to have a look at it?
edit: I am on windows Windows 10 Enterprise v22H2 with python 3.11.6
Hi,
I tested a simple composition relationship (Dolphin and DorsalFin) and it doesn't seem to work. Only inheritance relationship is captured in the resulting plantUML. Any idea how the composition relationship can be captured?
class Mammals:
def __init__(self):
pass
def eat(self):
pass
def move(self):
pass
class Dolphin(Mammals):
def __init__(self):
super(Dolphin, self).__init__()
self.fin = DorsalFin(length=1)
def move(self):
pass
class DorsalFin:
def __init__(self, length):
self.length = length
def wiggle(self):
pass
Being able to specify skinparams
as well as class diagram syntactic sugar for layout and arrow direction options for the output might be helpful.
Integrating it in the python API and reflecting it on the CLI needs first some design for usability.
I would be happy to work on this on my free time.
from collections import namedtuple
Circle = namedtuple('Circle', ['x', 'y', 'radius'], defaults=[1])
# Circle._fields: ('x', 'y', 'radius')
# Circle._fields_defaults: {'radius': 1}
Hey,
Here's a simple example that yield a ValueError:
class int64:
def __init__(self, value: int) -> None:
self.value: int = value
class Main:
def __init__(self):
self.a: list[int64] = [int64(0)] * 5
ValueError: list[int64] seems to be an invalid type annotation
I think fixing it is as simple as adding 0-9
in the compiled regex
Kinda critical for applications using numpy, as numpy types usually contain digits (i.e: numpy.typing.NDArray[numpy.float64]
)
Hi,
I have following file structure:
project/
โโโ my_model/
โโโ __init__.py
โโโ module_model_main.py
โโโ module_model_1.py
โโโ etcetera
โโโ template/
โโโ utility/
โโโ __init__.py
โโโ module_utility_1.py
โโโ etcetera
โโโ __init__.py
โโโ module_template_1.py
โโโ etcetera
โโโ __init__.py
โโโ etcetera
my_model/module_model_main.py
imports modules from:
template/module_template_1.py
(and etcetera
)template/utility/module_utility_1.py
(and etcetera
)my_model/module_model_1.py
(and etcetera
)where the latter imports from template/module_template_1.py
(and etcetera
) as well.
When running the code at the bottom of this post (from the project/
folder), I get a specific ModuleNotFoundError
:
File "[...]\my_model\module_model_main.py", line 17, in <module>
from my_model.module_model_1 import ClassModel1
File "[...]\my_model\module_model_1.py", line 1, in <module>
from template.module_template_1 import ClassTemplate1
File "[...]\template\module_template_1.py", line 9, in <module>
from utility.module_utility_1 import function_utility
ModuleNotFoundError: No module named 'utility'
At the highest execution level in my_model/module_model_main.py
where the error also occurs, py2puml
already succeeded past the following imports:
from template.module_template_1 import ClassTemplate1
from template.module_template_2 import ClassTemplate2
from template.module_template_3 import ClassTemplate3
from template.module_template_4 import ClassTemplate4
from template.module_template_5 import ClassTemplate5
from template.utility.module_utility_1 import ClassUtility1
As such, it errors on from my_model.module_model_1 import ClassModel1
. How can it be that py2puml
recognized template.utility
with the explicit imports but not within this line? Can it be because it is on the same level? Is there a solution?
Executed code:
from py2puml.py2puml import py2puml
import findspark
findspark.init() # Needed to allow finding pyspark as a module
print(''.join(
py2puml('my_model/', 'my_model')
))
Hi,
I'm able to execute your example, but was not able to run it without errors on my own code.
So, I made a small experiment. I placed a minimal class called myminiclass.py with the below content within the folder py2puml/domain
class MyMiniClass(object):
def __init__(self, instance_name: str):
self.name = instance_name
def get_name(self) -> str:
return self.name
The effect is now when running py2puml py2puml/domain py2puml.domain
I get the error below:
Traceback (most recent call last):
File "C:\Users\myuser\.conda\envs\myenv\lib\runpy.py", line 197, in _run_module_as_main
return _run_code(code, main_globals, None,
File "C:\Users\myuser\.conda\envs\myenv\lib\runpy.py", line 87, in _run_code
exec(code, run_globals)
File "C:\Users\myuser\.conda\envs\myenv\Scripts\py2puml.exe\__main__.py", line 7, in <module>
File "C:\Users\myuser\.conda\envs\myenv\lib\site-packages\py2puml\cli.py", line 17, in run
print(''.join(py2puml(args.path, args.module)))
File "C:\Users\myuser\.conda\envs\myenv\lib\site-packages\py2puml\py2puml.py", line 17, in py2puml
domain_item_module: ModuleType = import_module(name)
File "C:\Users\myuser\.conda\envs\myenv\lib\importlib\__init__.py", line 127, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
File "<frozen importlib._bootstrap>", line 984, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'py2puml.domain.myminiclass'
When I delete the file everything is working again for the example. Any ideas / suggestions?
Update:
I have one own python package which I installed in my conda environment. For this I'm able to run py2puml as long as I'm in the conda environment it is installed in. But when I create a new conda environment without it I get the same kind of error again.
py2puml fails to inspect a dataclass using Optional
as follows:
from dataclasses import dataclass
from typing import Optional
@dataclass
class MyData:
data: Optional[bool]
ValueError: Could not resolve type typing.Union in module <module 'tests.modules.withoptional' from '.../py2puml/tests/modules/withoptional.py'>: it needs to be imported explicitly.
It is because Optional[bool]
is dynamically converted into Union[bool, None]
without adding both typing.Union
and typing.Nonetype
in the module wrapped by the ModuleResolver.
With recent versions of PlantUML, the namespace section at the beginning of diagram files is not necessary anymore. Moreover, the visual grouping of namespaces (see namespace py2puml.domain {
) can be replaced by the !pragma useIntermediatePackages false
directive.
@startuml py2puml.domain
' [REMOVE]: this 'namespace' section is no longer necessary
namespace py2puml.domain {
namespace package {}
namespace umlclass {}
namespace umlitem {}
namespace umlenum {}
namespace umlrelation {}
}
' [ADD]: the following directive makes sure packages are grouped for readability sake
!pragma useIntermediatePackages false
' the contents can start here
class py2puml.domain.package.Package {
name: str
children: List[Package]
items_number: int
}
' [...]
class py2puml.domain.umlrelation.UmlRelation {
source_fqn: str
target_fqn: str
type: RelType
}
py2puml.domain.package.Package *-- py2puml.domain.package.Package
[...]
py2puml.domain.umlrelation.UmlRelation *-- py2puml.domain.umlrelation.RelType
footer Generated by //py2puml//
@enduml
That means that:
py2puml/export/namespace.py
can be removed from the codebase (and unit tests too)It is always good news when some code can be removed ๐
I have a feature request for this project that I would like to propose. I think, the addition of this feature would greatly enhance the project's functionality and usability.
The proposed feature is to incorporate a standardized function, such as doc, that enables retrieving the docstrings for classes and methods and seamlessly inserting them into the UML diagram. This addition would significantly contribute to clarifying the purpose and functionality of classes within the UML representation. It would provide developers with a convenient and consistent way to access the documentation, resulting in improved understanding and maintainability.
Furthermore, I am unsure whether this feature has been requested previously. If it has, I kindly request information regarding its current status or any ongoing efforts to implement it.
Hello!
I am an eager py2puml user and thanks to py2puml it is possible to create complex data structures without losing the overview!
I recently switched to use pydantic dataclasses instead of the "normal" dataclasses, because pydantic has a HUGE list of advantages over the standard dataclasses. The main advantage for me is:
The following example works already:
from pydantic.dataclasses import dataclass
@dataclass
class A:
number: int
Beautiful! Now, let's add a constraint to that:
from pydantic.dataclasses import dataclass
from pydantic import Field
@dataclass
class A:
positiveNumber: int = Field(gt=0)
Also that works! Although it's a pydantic specific feature, py2puml can still create the puml because pydantic uses the normal dataclass in the background. That is great!
But now, pydantic also provides types directly, like PositiveInt. Lets try it:
from pydantic.dataclasses import dataclass
from pydantic import Field, PositiveInt
@dataclass
class A:
positiveNumber: PositiveInt
This crashes!
File "C:\src\pkvgkv\.venv\Lib\site-packages\py2puml\parsing\compoundtypesplitter.py", line 60, in __init__
raise ValueError(f'{compound_type_annotation} seems to be an invalid type annotation')
ValueError: typing.Annotated[int, Gt(gt=0)] seems to be an invalid type annotation
Why is this a problem? I could just use the first method? Not in this example:
from pydantic.dataclasses import dataclass
from pydantic import Field, PositiveInt
@dataclass
class A:
positiveNumbers: list[PositiveInt]
Here I have to use the PositiveInt from pydantic, otherwise I can't constraint the elements of the list :(
In the background, pydantic creates these types with the following vanilla python functionality:
from annotated_types import Gt
from typing_extensions import Annotated
PositiveInt = Annotated[int, Gt(0)]
This means, if the following example compiles (without any pydantic code), then the pydantic code would also compile:
from annotated_types import Gt
from typing_extensions import Annotated
@dataclass
class A:
positiveNumber: Annotated[int, Gt(0)]
This means, if py2puml would support "Annotated" from typing_extensions, then we could use all the nice pydantic features with robust dataclasses and automatic range checking and everything and STILL have the great py2puml overview!
For me, it is not so important that the puml also shows the range etc. It would be totally sufficient as first goal to still compile and show that it is an int.
So:
That would be amazing!
I implemented a quick and dirty solution locally. In the compoundtypesplitter.py I imported
from re import sub as re_sub
and then in line 58 I added:
def __init__(self, compound_type_annotation: str, module_name: str):
## NEW CODE:
# Iterate over the annotation, replace all occurances of typing.Annotated[type, extraInfo]
# with type (drop the extra info for now to achieve goal 1)
while('typing.Annotated' in compound_type_annotation):
compound_type_annotation = re_sub(r'typing\.Annotated\[(.*?)\,(.*?)]', r'\1', compound_type_annotation)
# END OF NEW CODE
resolved_type_annotations = ...
This resolves goal 1 for me, but I think this is not a nice solution. Maybe the mapping of the typing.Annotated string to the internal type should not be done inside the CompoundTypeSplitter, as the CompoundTypeSplitter should already recieve the "clean" input. Also it may be luck that the CompoundTypeSplitter recieved the string in the first place, as the typing.Annotation itself is not a compound.
Anyway, this solution even works with more complicated pydantic structures like:
from pydantic import PositiveInt
@dataclass
class A:
number: Optional[list[list[PositiveInt]]]
Maybe you could take this example as idea and implement it correctly at the correct place? :)
I implemented now a version that is a bit less dirty (e.g. separate function) and it also not only works for pydantic types but also for custom types, e.g. these two variants will both work:
from pydantic import Field, PositiveInt
from typing_extensions import Annotated
@dataclass
class A:
number: list[Optional[PositiveInt]]
CrazyIntWithCustomConstraints = Annotated[int, Field(ge=2, lt=25)]
@dataclass
class B:
number: list[Optional[CrazyIntWithCustomConstraints]]
Please check #66 , thanks :)
Dear @lucsorel, @doyou89, @jonykalavera, @jjerphan,
While trying to generate UML content for "withrootnotincwd" module that is provided under /tests/modules/withrootnotincwd, I am encountering a ModuleNotFoundError.
Error below:
py2puml/asserts.py:15: in assert_py2puml_is_stringio
puml_content = list(py2puml(domain_path, domain_module))
py2puml/py2puml.py:12: in py2puml
inspect_package(
py2puml/inspection/inspectpackage.py:19: in inspect_package
domain_item_module: ModuleType = import_module(name)
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/importlib/__init__.py:127: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1014: in _gcd_import
???
<frozen importlib._bootstrap>:991: in _find_and_load
???
<frozen importlib._bootstrap>:975: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:671: in _load_unlocked
???
<frozen importlib._bootstrap_external>:783: in exec_module
???
<frozen importlib._bootstrap>:219: in _call_with_frames_removed
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
from dataclasses import dataclass
> from withrootnotincwd.point import Point
E ModuleNotFoundError: No module named 'withrootnotincwd'
tests/modules/withrootnotincwd/segment.py:3: ModuleNotFoundError
Note: This was identified by pytest.
After noticing this error, I tried running py2puml via CLI and encounter the same error:
(venv) supratiks-mbp:py2puml supratiksarkar$ py2puml tests/modules/withrootnotincwd/ tests.modules.withrootnotincwd
Traceback (most recent call last):
File "/Users/supratiksarkar/Desktop/Prod/venv/bin/py2puml", line 8, in <module>
sys.exit(run())
File "/Users/supratiksarkar/Desktop/Prod/venv/lib/python3.8/site-packages/py2puml/cli.py", line 24, in run
print(''.join(py2puml(args.path, args.module)))
File "/Users/supratiksarkar/Desktop/Prod/venv/lib/python3.8/site-packages/py2puml/py2puml.py", line 12, in py2puml
inspect_package(
File "/Users/supratiksarkar/Desktop/Prod/venv/lib/python3.8/site-packages/py2puml/inspection/inspectpackage.py", line 19, in inspect_package
domain_item_module: ModuleType = import_module(name)
File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/importlib/__init__.py", line 127, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
File "<frozen importlib._bootstrap>", line 991, in _find_and_load
File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 783, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/Users/supratiksarkar/Desktop/Prod/py2puml/tests/modules/withrootnotincwd/segment.py", line 3, in <module>
from withrootnotincwd.point import Point
ModuleNotFoundError: No module named 'withrootnotincwd'
In order to fix this error, I invested the segments.py file under test/modules/withrootnotincwd and notice that changing the following line:
from withrootnotincwd.point import Point
to
from .point import Point
fixes the issue.
Output after fixing the file:
(venv) supratiks-mbp:py2puml supratiksarkar$ py2puml tests/modules/withrootnotincwd/ tests.modules.withrootnotincwd
@startuml tests.modules.withrootnotincwd
namespace tests.modules.withrootnotincwd {
namespace point {}
namespace segment {}
}
class tests.modules.withrootnotincwd.point.Point {
x: float
y: float
}
class tests.modules.withrootnotincwd.segment.Segment {
a: Point
b: Point
}
tests.modules.withrootnotincwd.segment.Segment *-- tests.modules.withrootnotincwd.point.Point
footer Generated by //py2puml//
@enduml
(venv) supratiks-mbp:py2puml supratiksarkar$
Can you please take a look at it?
Thank you so much!
Issue to add support for SQLAlchemy models. This will include considerations about 1-1, 1-* and - relationships. Additionally, it should also consider inheritance.
A keyword parameter of the parent class have an annotation, but the type of this parameter does not revealed in the file of child class.
A.py
class SystemSession:
pass
class BaseShell:
def __init__(self, session: SystemSession = None):
self.session = session
self.is_started = False
B.py
from .A import BaseShell
class Nfvo(BaseShell):
pass
Traceback (most recent call last):
File "/home/doyou89/miniconda3/envs/jupyter/lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/home/doyou89/miniconda3/envs/jupyter/lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/__main__.py", line 4, in <module>
run()
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/cli.py", line 17, in run
print(''.join(py2puml(args.path, args.module)))
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/py2puml.py", line 18, in py2puml
inspect_module(
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/inspection/inspectmodule.py", line 61, in inspect_module
inspect_domain_definition(definition_type, root_module_name, domain_items_by_fqn, domain_relations)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/inspection/inspectmodule.py", line 46, in inspect_domain_definition
inspect_class_type(
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/inspection/inspectclass.py", line 97, in inspect_class_type
instance_attributes, compositions = parse_class_constructor(class_type, class_type_fqn, root_module_name)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/parsing/parseclassconstructor.py", line 29, in parse_class_constructor
visitor.visit(constructor_ast)
File "/home/doyou89/miniconda3/envs/jupyter/lib/python3.8/ast.py", line 371, in visit
return visitor(node)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/parsing/astvisitors.py", line 98, in generic_visit
NodeVisitor.generic_visit(self, node)
File "/home/doyou89/miniconda3/envs/jupyter/lib/python3.8/ast.py", line 379, in generic_visit
self.visit(item)
File "/home/doyou89/miniconda3/envs/jupyter/lib/python3.8/ast.py", line 371, in visit
return visitor(node)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/parsing/astvisitors.py", line 109, in visit_FunctionDef
self.generic_visit(node)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/parsing/astvisitors.py", line 98, in generic_visit
NodeVisitor.generic_visit(self, node)
File "/home/doyou89/miniconda3/envs/jupyter/lib/python3.8/ast.py", line 379, in generic_visit
self.visit(item)
File "/home/doyou89/miniconda3/envs/jupyter/lib/python3.8/ast.py", line 371, in visit
return visitor(node)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/parsing/astvisitors.py", line 149, in visit_Assign
self.extend_relations(full_namespaced_definitions)
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/parsing/astvisitors.py", line 82, in extend_relations
self.uml_relations_by_target_fqn.update({
File "/p4_ws/doyou89.jung/workspace/projects/github/py2puml/py2puml/parsing/astvisitors.py", line 85, in <dictcomp>
if target_fqn.startswith(self.root_fqn) and target_fqn not in self.uml_relations_by_target_fqn
AttributeError: 'NoneType' object has no attribute 'startswith'
In the parent class "BaseShell", session's type is "SystemSession".
But this "SystemSession" does not exist in the namespace of B.py
pyproject.toml
says python = "^3.7"
, but ast.get_source_segment
has been introduced in python 3.8. It doesn't work with 3.7:
File "/usr/local/lib/python3.7/dist-packages/py2puml/parsing/astvisitors.py", line 4, in <module>
from ast import (
ImportError: cannot import name 'get_source_segment' from 'ast' (/usr/lib/python3.7/ast.py)
Pyreverse is a command included with pylint
that analyzes your source code and generates package and class diagrams.
Since version 2.10.0 it includes a plantuml printer https://pylint.pycqa.org/en/latest/whatsnew/changelog.html?highlight=plantuml#what-s-new-in-pylint-2-10-0
The current approach detects class variables (which is how dataclass works). But non-dataclass classes should be handled as well.
When using Union types annotations, e.g. int | float
the script crashes with an error
File "/xxxx/py2puml/parsing/astvisitors.py", line 215, in shorten_compound_type_annotation
ValueError: Could not resolve type int | float in module
It'd be useful to be able to support this.
The link for Poetry in the Install section on https://pypi.org/project/py2puml/ goes to https://pipenv.readthedocs.io/en/latest/ rather than https://python-poetry.org/.
I humbly apologize for an absolute newbie question, but I seem to be unable to get py2puml to produce any usable result (apart from empty "@startuml/@enduml"):
As a work in progress, I have following file structure:
C:\Folder_A\Folder_AA\Folder_Project\module1.py
C:\Folder_A\Folder_AA\Folder_Project\module2.py
C:\Folder_A\Folder_AA\Folder_Project\subfolder\module3.py
What am I to enter into path and module parameters to get an output of all classes used in all three modules?
In my first attempt to use the py2puml CLI, I encountered an import problem similar to #21. When I tried to use a relative path for the path argument, the fix from #40 didn't help because the source directory was not in the current directory.
I wrote a small patch for cli.py
that fixed the problem for me:
--- a/py2puml/cli.py
+++ b/py2puml/cli.py
@@ -9,11 +9,6 @@ from py2puml.py2puml import py2puml
def run():
- # adds the current working directory to the system path so that py2puml can import them
- current_working_directory = str(Path.cwd().resolve())
- if current_working_directory not in path:
- path.append(current_working_directory)
-
argparser = ArgumentParser(description='Generate PlantUML class diagrams to document your Python application.')
argparser.add_argument('-v', '--version', action='version', version='py2puml 0.7.2')
@@ -21,4 +16,10 @@ def run():
argparser.add_argument('module', metavar='module', type=str, help='the module name of the domain', default=None)
args = argparser.parse_args()
+
+ # add the directory containing the package to the system path so that py2puml can import it
+ directory = str(Path(args.path).parent.resolve())
+ if directory not in path:
+ path.append(directory)
+
print(''.join(py2puml(args.path, args.module)))
Obviously I'm not aware of all the use cases, but I wonder if the CLI argument list could be simplified: If the order of arguments was changed, may be the path argument might not be required if the package was installed or the source directory (with the package's name) was contained in the current directory. If the current directory happened to be a project directory (i.e. contains setup.py
or equivalent), may be the project could be the default and no argument would be required at all.
I look for some interactive way to create such diagrams (with context menu options on nodes, drag / drop) etc.
So I need events to handle commands.
Is it possible/intended to generate such event handlers on the py2uml outputs?
Thanks
I am trying to analyze an inherited project, with a structure:
package
subpackage
__init__.py # base classes and interfaces
classfile.py # derived classes
If I run: py2puml package/subpackage package.subpackage
it will find any classes defined in classfile.py
, but will not list any classes from the __init__.py
Since the entire project is organized this way, and it's not my code, I am unable to use py2puml on it.
Hi there,
I've tried the Pynguin to generated the test cases for Package.py
And there is one test case that mark as expected fail by Pynguin.
import pytest
import package as module_0
import dataclasses as module_1
@pytest.mark.xfail(strict=True)
def test_case_0():
module_0.Package(123, [], 123)
And after tested with Pytest, it is throw the error that this case was mark as XPASS(Unexpected pass)
So I checked the Package.py function and found out that the there's no raise type error in there, that's why the Pynguin expected the test cases as failed.
from dataclasses import dataclass, field
from typing import List
@dataclass
class Package:
'''A folder or a python module'''
name: str
children: List['Package'] = field(default_factory=list)
items_number: int = 0
# def __post_init__(self):
# if not isinstance(self.name, str):
# raise TypeError("Name must be a string.")
# suggestion #
Thank you.
Hi!
First of all, really liked the idea of the project. Auto-generating plantuml documentation straight from Python code is quite nice.
I've attempted to check out py2puml on my project, but happened to encounter this error:
ValueError: typing.Literal['val1', 'val2'] seems to be an invalid type annotation
I'm assuming py2puml is currently unable to recognize literals (which essentially are a form of enums), and based on this PR it also can't yet work with Pydantic models, which subjectively is even more concerning.
Is there a chance for the missing type (and potentially other types I haven't gotten an issue with yet) to be added to py2puml?
Again, really like the idea, can see myself using this very often if it works out :)
Classes created with collections.namedtuple
carry no type annotations.
from collections import namedtuple
Function = namedtuple('Function', ['name', 'module_Path'])
py2puml
would generate the following documentation:
@startuml
class Function {
name: Any
module_path: Any
}
@enduml
However, it is also possible to create named tuple classes with typing.NamedTuple
and specify type annotations:
from typing import NamedTuple, Tuple
class Function(NamedTuple):
'''
Models a function (calling or being called):
- it has a name
- it is hosted in a module, modeled by its path as a tuple of strings
'''
name: str
module_path: Tuple[str]
The expected PlantUML documentation would then be:
@startuml
class Function {
name: str
module_path: Tuple[str]
}
@enduml
Hello, I'm getting an AttributeError
and I don't quite know how to fix the traversed code.
py2puml
works great, but I'm trying to use it to clean up a very imperfect codebase and I've had to make some adjustments to the codebase for successful py2puml
output (which is fine). I don't know what I need to adjust to fix the current error though.
running py2puml manim/mobject manim.mobject
on manim
@c69f6eb yields:
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/bin/py2puml", line 8, in <module>
sys.exit(run())
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/cli.py", line 17, in run
print(''.join(py2puml(args.path, args.module)))
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/py2puml.py", line 18, in py2puml
inspect_module(
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/inspection/inspectmodule.py", line 59, in inspect_module
inspect_domain_definition(definition_type, root_module_name, domain_items_by_fqn, domain_relations)
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/inspection/inspectmodule.py", line 46, in inspect_domain_definition
inspect_class_type(
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/inspection/inspectclass.py", line 97, in inspect_class_type
instance_attributes, compositions = parse_class_constructor(class_type, class_type_fqn, root_module_name)
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/parsing/parseclassconstructor.py", line 39, in parse_class_constructor
visitor.visit(constructor_ast)
File "/usr/local/Cellar/[email protected]/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ast.py", line 407, in visit
return visitor(node)
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/parsing/astvisitors.py", line 98, in generic_visit
NodeVisitor.generic_visit(self, node)
File "/usr/local/Cellar/[email protected]/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ast.py", line 415, in generic_visit
self.visit(item)
File "/usr/local/Cellar/[email protected]/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ast.py", line 407, in visit
return visitor(node)
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/parsing/astvisitors.py", line 108, in visit_FunctionDef
self.generic_visit(node)
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/parsing/astvisitors.py", line 98, in generic_visit
NodeVisitor.generic_visit(self, node)
File "/usr/local/Cellar/[email protected]/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ast.py", line 415, in generic_visit
self.visit(item)
File "/usr/local/Cellar/[email protected]/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ast.py", line 407, in visit
return visitor(node)
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/parsing/astvisitors.py", line 98, in generic_visit
NodeVisitor.generic_visit(self, node)
File "/usr/local/Cellar/[email protected]/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ast.py", line 415, in generic_visit
self.visit(item)
File "/usr/local/Cellar/[email protected]/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/ast.py", line 407, in visit
return visitor(node)
File "/Users/citrus/Library/Caches/pypoetry/virtualenvs/manim-fDwhioqp-py3.9/lib/python3.9/site-packages/py2puml/parsing/astvisitors.py", line 126, in visit_Assign
variables_collector = AssignedVariablesCollector(self.class_self_id, None)
AttributeError: 'ConstructorVisitor' object has no attribute 'class_self_id'
I have success with
py2puml manim/mobjects/svg manim.mobjects.svg
manim/mobject/svg/opengl_tex_mobject.py
and manim/mobject/svg/opengl_text_mobject.py
py2puml manim/mobject/types manim.mobject.types
so maybe that narrows the issue down slightly ๐ .
I think it would be great to add the ability to get the docstrings for classes and or methods and insert them into the uml. This would go a long way in clarifying what classes are actually for in the uml.
The __doc__
attribute can be used to get the docstrings without much trouble.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.