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

Add Type Check Back to Refactor CI #630

Merged
merged 4 commits into from
Jul 8, 2024

Conversation

MattToast
Copy link
Member

Addresses type errors found in the SmartSim core refactor branch. Re-adds a static type check back into the CI to allow for gradual typing as more parts of the API become more concrete

@MattToast MattToast added the area: CI/CD Issues related to continuous integration and deployment label Jul 3, 2024
@MattToast MattToast self-assigned this Jul 3, 2024
Copy link

codecov bot commented Jul 3, 2024

Codecov Report

Attention: Patch coverage is 69.13580% with 25 lines in your changes missing coverage. Please review.

Please upload report for BASE (smartsim-refactor@e9d8eca). Learn more about missing BASE report.

Files with missing lines Patch % Lines
smartsim/launchable/baseJobGroup.py 55.55% 8 Missing ⚠️
smartsim/launchable/colocatedJobGroup.py 58.33% 5 Missing ⚠️
smartsim/launchable/jobGroup.py 58.33% 5 Missing ⚠️
smartsim/types.py 0.00% 3 Missing ⚠️
smartsim/experiment.py 0.00% 1 Missing ⚠️
smartsim/launchable/mpmdjob.py 85.71% 1 Missing ⚠️
smartsim/settings/batchSettings.py 0.00% 1 Missing ⚠️
smartsim/settings/launchSettings.py 75.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                 Coverage Diff                  @@
##             smartsim-refactor     #630   +/-   ##
====================================================
  Coverage                     ?   32.89%           
====================================================
  Files                        ?      106           
  Lines                        ?     6447           
  Branches                     ?        0           
====================================================
  Hits                         ?     2121           
  Misses                       ?     4326           
  Partials                     ?        0           
Files with missing lines Coverage Δ
smartsim/entity/entity.py 56.66% <100.00%> (ø)
smartsim/entity/model.py 32.38% <ø> (ø)
smartsim/error/errors.py 58.33% <ø> (ø)
smartsim/launchable/basejob.py 100.00% <100.00%> (ø)
smartsim/launchable/job.py 69.56% <100.00%> (ø)
smartsim/settings/__init__.py 100.00% <100.00%> (ø)
smartsim/settings/builders/batch/__init__.py 100.00% <ø> (ø)
smartsim/experiment.py 29.41% <0.00%> (ø)
smartsim/launchable/mpmdjob.py 41.30% <85.71%> (ø)
smartsim/settings/batchSettings.py 74.07% <0.00%> (ø)
... and 5 more

@@ -8,14 +10,11 @@
from .basejob import BaseJob


class BaseJobGroup(Launchable, MutableSequence):
Copy link
Contributor

Choose a reason for hiding this comment

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

That was my bad, nice catch!

Copy link
Contributor

Choose a reason for hiding this comment

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

How did the tests still pass even though I did not inherit the ABC class?

Copy link
Member Author

Choose a reason for hiding this comment

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

Without going too far into the weeds of how ABC and abstractmethod work, the @abstractmethod decorator just sets a __isabstractmethod__ = True attr to the method.

class C:
    @abstractmethod
    def some_method(self):  print("Totally works!")

C().some_method()  # prints: "Totally works!"
C().some_method.__isabstractmethod__  # returns: `True`

The ABC class is what actually enforces that no methods have a __isabstractmethod__ set to true when the class is created:

class C(ABC):
    @abstractmethod
    def some_method(self): ...

C()  # Now cannot be instanced, raises a `TypeError`

So basically, it got past the tests precisely because it did not inherit from ABC!

raise TypeError("Can only assign a `BaseJob`")
self.jobs[idx] = deepcopy(value)
else:
if not isinstance(value, t.Iterable):
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you need to check here that its an iterable of BaseJobs?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we want to keep it type safe at runtime, we unfortunately do :(.

From the overloads, we can tell that when idx is a slice that value should be a Iterable, but unfortunately that assumed constraint is not actually guaranteed at runtime, and thus the type checker will complain for the definition of this method if we do not explicitly handle it.


def __setitem__(self, idx: int, value: BaseJob) -> None:
@t.overload
def __setitem__(self, idx: int, value: BaseJob) -> None: ...
Copy link
Contributor

Choose a reason for hiding this comment

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

why is setitem repeated twice and what is t.overload?

Copy link
Member Author

Choose a reason for hiding this comment

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

__setitem__ is actually defined here 3 times! The first two definitions give the different signatures to the type checker to provide more information about how a method should be used. Only the last one is the one that matters for runtime though.

Python does not have proper method overloading (each method has one, and exactly one, definition), but the @t.overload decorator informs the type checker that depending on how the method is called there are really many (in this case two) discrete signatures. This allows for python to "spoof" true method overloading at type check time.

Now if you index into the job group with an int mypy knows that you should be trying to assign a BaseJob, but if you index into the job group with a slice, mypy knows that it should expect an Iterable[BaseJob]

@@ -26,9 +31,19 @@ def jobs(self) -> t.List[BaseJob]:
"""
return self._jobs

def __str__(self): # pragma: no-cover
@t.overload
def __getitem__(self, idx: int) -> BaseJob: ...
Copy link
Contributor

Choose a reason for hiding this comment

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

repeated dunder methods again, hmmmm

Copy link
Member Author

Choose a reason for hiding this comment

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

Similar to the __setitem__ conversation, only the last definition matters at runtime, but these other methods provide more information to the type checker.

Now if a user calls this method with different indexes, mypy is able to infer what type you should get back

reveal_type(job_group[0])    # reveals: `int`
reveal_type(job_group[1:3])  # reveals: `JobGroup`

but do note that it is still up to the author to make sure that the actual definition conforms to the method overloads. In this case, it's basically a fancy way to the the type checker "trust me: when you get this input you can expect this output".

class DragonRunSettings:
pass
class SettingsBase:
def __init__(self, *_: t.Any, **__: t.Any) -> None: ...
Copy link
Contributor

Choose a reason for hiding this comment

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

why did u define dunders for base?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just to get past the type check without ignoring all of _core. There are still a few places where settings are instanced using one of these stubclasses. If we do not define a constructor that takes any number of args and any number of kwargs, the type checker will complain anytime a settings is instanced that does not conform to the signature

Copy link
Member Author

Choose a reason for hiding this comment

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

Similarly, the __getattr__ is there so that the type checker does not complain when try to dot-access an attr that does not directly exist on these stub classes.

@@ -10,8 +10,13 @@
app_3 = Application("app_3", "python", run_settings=LaunchSettings("slurm"))


class MockJob(BaseJob):
Copy link
Contributor

Choose a reason for hiding this comment

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

why did you switch out BaseJob for MockJob?

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to provide a concrete implementation of the BaseJob abstract class. Otherwise we cannot instance the class we are testing!

@MattToast MattToast merged commit 7e3e92c into CrayLabs:smartsim-refactor Jul 8, 2024
34 of 35 checks passed
amandarichardsonn pushed a commit to amandarichardsonn/SmartSim that referenced this pull request Jul 17, 2024
Addresses type errors found in the SmartSim core refactor branch.
Re-adds a static type check back into the CI to allow for gradual typing
as more parts of the API become more concrete

[ committed by @MattToast ]
[ reviewed by @amandarichardsonn ]
amandarichardsonn pushed a commit to amandarichardsonn/SmartSim that referenced this pull request Jul 17, 2024
Addresses type errors found in the SmartSim core refactor branch.
Re-adds a static type check back into the CI to allow for gradual typing
as more parts of the API become more concrete

[ committed by @MattToast ]
[ reviewed by @amandarichardsonn ]
@MattToast MattToast deleted the type-errs branch August 26, 2024 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: CI/CD Issues related to continuous integration and deployment ignore-for-release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants