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 separate constructor for CustomParticle
and improve |ParticleList| compatibility with |Quantity| objects
#1874
Conversation
Thank you for contributing to PlasmaPy! The project's future depends deeply on contributors like you, so we deeply appreciate it! 🌱 The following checklist will be used by the code reviewer to help guide the code review process.
|
This PR is still in its early stages, and I'm still thinking about the implementation details. I'm going to try to reduce breaking changes, but it's likely this would make |
Codecov ReportBase: 98.31% // Head: 98.31% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## main #1874 +/- ##
==========================================
- Coverage 98.31% 98.31% -0.01%
==========================================
Files 95 95
Lines 8383 8424 +41
==========================================
+ Hits 8242 8282 +40
- Misses 141 142 +1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
CustomParticle
CustomParticle
and improve |ParticleList| compatibility with |Quantity| objects
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.
Could use some cleaning up imo!
5 * u.kg, | ||
5 * u.C, |
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.
So these two will create, respectively, a charge-less 5kg chonkyboi of a particle and a massless one with a massive 5 C charge, both custom ones, right?
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.
Yep!
plasmapy/particles/_factory.py
Outdated
for particle_type in (Particle, CustomParticle, ParticleList): | ||
with contextlib.suppress(TypeError, InvalidParticleError): | ||
with contextlib.suppress(TypeError, InvalidParticleError, ValueError): | ||
|
||
# Do not interpret a string as the first positional argument | ||
# as the symbol for a CustomParticle or something to be | ||
# iterated over for a ParticleList. | ||
if particle_type is not Particle and args and isinstance(args[0], str): | ||
continue | ||
|
||
return particle_type(*args, **kwargs) |
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.
Might be myself being tired but this whole block seems difficult to reason about! This needs a refactor:
- let's enter
suppress
context once instead of within the for loop - the case for
Particle
should probably be separate
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.
Yeah, this block is a bit hard to wrap one's mind around.
The reason why contextlib.suppress
is used within the for
block is that it tries to create a Particle
, CustomParticle
, and ParticleList
sequentially. There's a chance for one of these exceptions to be raised each time. If the suppress
context were outside the for
loop, then it would exit the context environment upon the first exception...I think. If it failed to create a Particle
, then it wouldn't try to create a CustomParticle
or ParticleList
afterwards.
I'll keep thinking about how to clean this up and reduce the cognitive complexity.
*quantities : tuple of ~astropy.units.Quantity or `str` | ||
The mass, electrical charge, and/or symbol of the custom | ||
particle, provided as positional arguments. The mass and charge | ||
may be provided in either order, but the symbol may only be the | ||
final positional argument. | ||
|
||
charge : ~astropy.units.Quantity or ~numbers.Real, optional | ||
The electric charge of the custom particle. If provided as a | ||
`~astropy.units.Quantity`, then it must be in units of electric | ||
charge. Defaults to |nan| C. | ||
charge : ~astropy.units.Quantity or ~numbers.Real, |keyword-only|, optional | ||
The electrical charge of the custom particle, if not provided in | ||
``quantities`` as a positional argument. | ||
|
||
Z : ~numbers.Real, optional, |keyword-only| | ||
The :term:`charge number`, which is equal to the ratio of the | ||
charge to the elementary charge. | ||
mass : ~astropy.units.Quantity, |keyword-only|, |keyword-only|, optional | ||
The mass of the custom particle, if not provided in | ||
``quantities`` as a positional argument. | ||
|
||
symbol : str, |keyword-only|, |keyword-only|, optional | ||
The symbol to be assigned to the custom particle, if not | ||
provided as the final positional argument. | ||
|
||
symbol : str, optional | ||
The symbol to be assigned to the custom particle. | ||
Z : ~numbers.Real, optional, |keyword-only| | ||
The |charge number|, which is equal to the ratio of the charge | ||
to the elementary charge. |
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 kind of dislike how this whole change actually made the user-facing complexity, as measured by the length of docstring necessary, higher! Nothing to change here, probably, just voicing my concern.
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.
Agreed — the downside of this change is the increased user-facing complexity. My original plan was to deprecate the mass
and charge
keyword arguments, which I haven't done but am still considering. The extra complexity comes from attempting to preserve backward compatibility where mass
and charge
can be used as keyword arguments too.
plasmapy/particles/particle_class.py
Outdated
if isinstance(quantities[-1], str) and symbol is None: | ||
symbol = quantities[-1] | ||
quantities = list(quantities[:-1]) | ||
|
||
try: | ||
physical_type_dict = _get_physical_type_dict( | ||
quantities, | ||
only_quantities=True, | ||
strict=True, | ||
allowed_physical_types={ | ||
u.physical.mass, | ||
u.physical.electrical_charge, | ||
}, | ||
) | ||
except (TypeError, ValueError) as exc: | ||
raise InvalidParticleError( | ||
"Unable to create a CustomParticle based off of the " | ||
"following positional arguments, which should be a" | ||
f"Quantity representing electrical charge and/or a" | ||
f"Quantity representing mass: {quantities}." | ||
) from exc | ||
|
||
if u.physical.electrical_charge in physical_type_dict: | ||
if charge is not None: | ||
raise TypeError( | ||
"Cannot provide charge as both a positional and keyword " | ||
"argument." | ||
) | ||
charge = physical_type_dict[u.physical.electrical_charge] | ||
|
||
if u.physical.mass in physical_type_dict: | ||
if mass is not None: | ||
raise TypeError( | ||
"Cannot provide mass as both a positional and keyword argument." | ||
) | ||
mass = physical_type_dict[u.physical.mass] | ||
|
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.
Since this whole block is pretty separate from other functionality, how about moving it to a separate semi-private constructor like this...
@classmethod
def _from_positional(cls, args):
... # this whole block
and then calling that from here if quantities
is non-empty?
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.
Agreed. This would be cleaner by extracting a bunch of this into its own method.
My main question at this point is: would it be better to deprecate the |
try: | ||
return Particle(*args, **kwargs) | ||
except _particle_creation_exceptions: | ||
# If the first argument is a string but is not a valid particle, | ||
# then don't try to create a CustomParticle or ParticleList. | ||
if args and isinstance(args[0], str): | ||
raise | ||
|
||
for particle_type in (CustomParticle, ParticleList): | ||
with contextlib.suppress(*_particle_creation_exceptions): | ||
return particle_type(*args, **kwargs) |
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.
Hm...it's looking like I'll need to restructure this part again in #1867 to enable @particle_input
to take values for Z
that are real but not integers.
Can we instead hold off on this until we've discussed #1873 in more detail? I'm writing up some thoughts on it right now, and we do have a telecon today, right? |
CustomParticle
and improve |ParticleList| compatibility with |Quantity| objectsCustomParticle
and improve |ParticleList| compatibility with |Quantity| objects
The approach suggested by @StanczakDominik at today's telecon is to implement a pattern like: class CustomParticle:
def __init__(self, mass: u.kg, charge: u.C):
# current broad-strokes signature of CustomParticle creator
pass
@classmethod
def _from_positionals(cls, *args):
# use this from @particleinput
... # figure out physical type of argument -> create a dict -> initialize particle
d = dict(mass = mass_value_kg, charge = charge_value_C)
return cls(**d) I think I might try this in a separate PR... |
This PR makes the following changes:
CustomParticle
toCustomParticle(*quantities, ...)
, wherequantities
can includeQuantity
objects of physical type mass or charge, or (as its last item) the symbol. See Add mechanism to instantiateCustomParticle
based off of positional arguments #1873 for some motivation and follow-up discussion. This is a follow-up attempt to do more or less the same thing as Provide more flexibility inCustomParticle
arguments #1364, which didn't get merged. Closes Add mechanism to instantiateCustomParticle
based off of positional arguments #1873.Particle
and not as a symbol forCustomParticle
.ParticleList.__init__
accept an iterable containingQuantity
objects. Closes MakeParticleList
compatible withQuantity
objects with physical type of mass or electrical charge #1872.ParticleList
methods acceptQuantity
objects of mass or electrical charge.The changes to
CustomParticle
made the changes toParticleList
pretty straightforward to implement, so I addressed them here.This PR should make it easier to address #1869 and #1871.