-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug bash improvements to Python DX, better error messages #1346
Conversation
70e26d1
to
0e73efd
Compare
if utils.issubclass_safe(sym, definitions.ABCChainlet) | ||
and cast(definitions.ABCChainlet, sym).meta_data.is_entrypoint | ||
} | ||
class _ABCImporter(abc.ABC): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally think this is another good use case for inheritance, since otherwise we'd end up passing lots of function arguments around to account for differences (no_entrypoint_error, multiple_entrypoints_error, target_cls_type, and any in the future)
However, I also brought all dependencies of _import_target
inside this class, like _get_entrypoint_chainlets
and _load_module
. Since nobody else needs those functions, I personally like encapsulating within the class to better indicate who relies on these. An additional benefit (in the future) could be to more easily extract this class to a standalone file if we want to break up the large framework.py
one day
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
0e73efd
to
7f7d449
Compare
@classmethod | ||
def no_entrypoint_error(cls, module_path: pathlib.Path) -> ValueError: | ||
return ValueError( | ||
f"No Model class in `{module_path}` inherits from {cls.target_cls_type()}." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, the error message would point to ABCChainlet, but users aren't interacting with that class directly. I think that including the public_api class is slightly better here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. It think the reason was the cyclic dependency issue... let's fix both this time.
} | ||
|
||
@classmethod | ||
def _load_module(cls, module_path: pathlib.Path) -> tuple[types.ModuleType, Loader]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_load_module and _cleanup_module_imports have no changes, other than the ones needed to make them class methods
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I trust you on this, I'm not gonna do a line-by-line review this time ;)
try: | ||
@classmethod | ||
@abc.abstractmethod | ||
def target_cls_type(cls) -> Type[definitions.ABCChainlet]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having an intended target type solves https://basetenlabs.slack.com/archives/C033TL5SLH0/p1738087812972669?thread_ts=1738086363.048469&cid=C033TL5SLH0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be private though (and also others)?
@@ -773,7 +773,7 @@ def gen_truss_chainlet( | |||
gen_root = pathlib.Path(tempfile.gettempdir()) | |||
chainlet_dir = _make_chainlet_dir(chain_name, chainlet_descriptor, gen_root) | |||
logging.info( | |||
f"Code generation for Chainlet `{chainlet_descriptor.name}` " | |||
f"Code generation for {chainlet_descriptor.chainlet_cls.entity_type} `{chainlet_descriptor.name}` " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change switches between Model / Chainlet depending on the context
truss-chains/tests/test_framework.py
Outdated
@@ -2,6 +2,7 @@ | |||
import contextlib | |||
import logging | |||
import re | |||
from pathlib import Path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please stick to convention to not import symbols (except typing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh sorry, my editor uses this convention by default. I'll investigate a way to change that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if you just type pathlitb.Path
at the call site - will it add import pathlib
automatically? Or will it create from pathlib import Path
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately doesn't auto import with the pathlib.Path
syntax, but I have a unique dev setup locally - I can probably fix this!
@@ -0,0 +1,7 @@ | |||
class PassthroughModel: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got confused by this name (and didn't see at first that the base class is missing). What about "NonModelClass" or "NonChainletClass" (and also update the file name)?
if utils.issubclass_safe(sym, definitions.ABCChainlet) | ||
and cast(definitions.ABCChainlet, sym).meta_data.is_entrypoint | ||
} | ||
class _ABCImporter(abc.ABC): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
@@ -35,7 +36,7 @@ | |||
import pydantic | |||
from typing_extensions import ParamSpec | |||
|
|||
from truss_chains import definitions, utils | |||
from truss_chains import definitions, public_api, utils |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid cyclic dependencies somehow?
Maybe ChainletBase
should not be directly in public
API.
It might fit into definitions
. But the goal there was to have mostly constants, interfaces and datastructures (i.e. no implementaiton logic) -
which is already not well fulfilled, because it does contain some impl.
Maybe it's time to move ChainletBase
and some stuff from definitions
either into framework or a new module (depending on what makes more sense).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which ever you chose, the dependencies should be somewhat straight...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
framework
requires these classes for validation / error messages, and these classes require framework
for initialization logic, so for now I think it actually makes sense for them all to belong in framework
? I don't have too many opinions on the future of definitions / public_api, but I think framework
needs to be split up into multiple modules / files.
We will need to grab these from framework
when choosing what to expose from truss_chains
now, but since public_api
already depends on framework
I don't think this is too bad. However, don't have much experience with python import management, so open to thoughts here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, then move ChainletBase
to framework
and add an alias in public_api
.
Generally we should follow this (for imports and other things): https://google.github.io/styleguide/pyguide.html
But it has the drawback that it is very much focussed on "internal libs" in a monorepo rather than OSS packages for "external endusers" so there are a few places when it comes exposing symbols where to deviated from that guide (e.g. generally it says you should not create aliases, but I think for a public API it's needed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I was considering an alias but decided against it because the truss_chains
already felt like an alias for things across public_api / definitions / framework (now). Can add though!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, no alias in public_api
, just directly to __init__.py
then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sweet, that's what I have currently! Will merge as is then
try: | ||
@classmethod | ||
@abc.abstractmethod | ||
def target_cls_type(cls) -> Type[definitions.ABCChainlet]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be private though (and also others)?
} | ||
|
||
@classmethod | ||
def _load_module(cls, module_path: pathlib.Path) -> tuple[types.ModuleType, Loader]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I trust you on this, I'm not gonna do a line-by-line review this time ;)
@classmethod | ||
def no_entrypoint_error(cls, module_path: pathlib.Path) -> ValueError: | ||
return ValueError( | ||
f"No Model class in `{module_path}` inherits from {cls.target_cls_type()}." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. It think the reason was the cyclic dependency issue... let's fix both this time.
2568a20
to
d88e360
Compare
🚀 What
This is a first pass at improving the new python DX after our bug bash:
framework.import_target
will now be appropriately scoped to either a model or chainlet targetpublic_api
rather than our abstractABCChainlet
Next steps:
💻 How
🔬 Testing