Skip to content
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

Merged
merged 2 commits into from
Jan 29, 2025

Conversation

nnarayen
Copy link
Contributor

@nnarayen nnarayen commented Jan 28, 2025

🚀 What

This is a first pass at improving the new python DX after our bug bash:

  • Validation that runs during framework.import_target will now be appropriately scoped to either a model or chainlet target
  • In error message, we surface the classes that users interact with in public_api rather than our abstract ABCChainlet

Next steps:

  • Improve error messages related to code examples

💻 How

🔬 Testing

@nnarayen nnarayen force-pushed the nikhil/python-dx-improvements branch 2 times, most recently from 70e26d1 to 0e73efd Compare January 28, 2025 20:51
if utils.issubclass_safe(sym, definitions.ABCChainlet)
and cast(definitions.ABCChainlet, sym).meta_data.is_entrypoint
}
class _ABCImporter(abc.ABC):
Copy link
Contributor Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

@nnarayen nnarayen force-pushed the nikhil/python-dx-improvements branch from 0e73efd to 7f7d449 Compare January 28, 2025 20:59
@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()}."
Copy link
Contributor Author

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

Copy link
Contributor

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]:
Copy link
Contributor Author

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

Copy link
Contributor

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]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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}` "
Copy link
Contributor Author

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

@@ -2,6 +2,7 @@
import contextlib
import logging
import re
from pathlib import Path
Copy link
Contributor

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)

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

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:
Copy link
Contributor

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):
Copy link
Contributor

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
Copy link
Contributor

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).

Copy link
Contributor

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...

Copy link
Contributor Author

@nnarayen nnarayen Jan 29, 2025

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!

Copy link
Contributor

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).

Copy link
Contributor Author

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!

Copy link
Contributor

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?

Copy link
Contributor Author

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]:
Copy link
Contributor

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]:
Copy link
Contributor

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()}."
Copy link
Contributor

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.

@nnarayen nnarayen force-pushed the nikhil/python-dx-improvements branch from 2568a20 to d88e360 Compare January 29, 2025 15:31
@nnarayen nnarayen merged commit 7812b1d into main Jan 29, 2025
5 checks passed
@nnarayen nnarayen deleted the nikhil/python-dx-improvements branch January 29, 2025 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants