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

Python style fixes #484

Merged
merged 5 commits into from
Nov 11, 2017
Merged

Python style fixes #484

merged 5 commits into from
Nov 11, 2017

Conversation

gabrielkrell
Copy link
Contributor

Checklist

  • I have made a material change to the repo (functionality, testing, spelling, grammar)
  • I have read the [Contribution Guide] and my PR follows them.
  • I updated my branch with the master branch.
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation about the functionality in the appropriate .md file
  • I have added in line documentation to the code I modified

Short description of what this PR does:

  • Cleans up Mail helper: reduces line count, uses Pythonic defaults
  • See commit notes for categories of fixes.

These things were just bugging me a bit. There's probably a "terser is better" or something in the Python Zen.

Note: I think existing tests should cover this since I didn't change any functionality, but I'll let Codecov yell at me if not. Tests pass with tox.

Initializers should use property setters like everyone else.  The default value of None can be applied with the existing default parameters instead of the manually setting internal values to None.
A sensible default for a list property is an empty list.  This change simplifies existing code because we no longer need to turn a None into [] when using an add() method for the first time, and checks in get()s are simpler.
 - move import to top of file for consistency
 - simplify __init__ logic: make it more clear that there are two categories of input (old v. new-style), and take advantage of default Nones.
@thinkingserious thinkingserious added the status: code review request requesting a community code review or review from Twilio label Oct 29, 2017
@codecov-io
Copy link

codecov-io commented Oct 29, 2017

Codecov Report

❗ No coverage uploaded for pull request base (master@1ab4285). Click here to learn what that means.
The diff coverage is 95.77%.

Impacted file tree graph

@@           Coverage Diff            @@
##             master    #484   +/-   ##
========================================
  Coverage          ?   85.1%           
========================================
  Files             ?      30           
  Lines             ?     920           
  Branches          ?     108           
========================================
  Hits              ?     783           
  Misses            ?      84           
  Partials          ?      53
Impacted Files Coverage Δ
sendgrid/helpers/mail/bcc_settings.py 90% <100%> (ø)
sendgrid/helpers/mail/substitution.py 94.11% <100%> (ø)
sendgrid/helpers/mail/content.py 89.47% <100%> (ø)
sendgrid/helpers/mail/footer_settings.py 88.46% <100%> (ø)
sendgrid/helpers/mail/bypass_list_management.py 91.66% <100%> (ø)
sendgrid/helpers/mail/asm.py 78.94% <100%> (ø)
sendgrid/helpers/mail/click_tracking.py 89.47% <100%> (ø)
sendgrid/helpers/mail/sandbox_mode.py 91.66% <100%> (ø)
sendgrid/helpers/mail/spam_check.py 88.46% <100%> (ø)
sendgrid/helpers/mail/email.py 97.29% <100%> (ø)
... and 9 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1ab4285...d94fdb6. Read the comment docs.

@thinkingserious thinkingserious added difficulty: medium fix is medium in difficulty hacktoberfest labels Oct 29, 2017
Copy link
Contributor

@delirious-lettuce delirious-lettuce left a comment

Choose a reason for hiding this comment

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

@gabrielkrell ,

I'm curious as to what this PR really accomplishes. There seems to be a pattern in these classes:

  • assign a "private" value(s) to None (corresponds to a public property)
  • if a corresponding argument was not passed in to __init__, call the setter

This pattern is also what is shown in the Python docs on properties:

class Parrot:
    def __init__(self):
        self._voltage = 100000

    @property
    def voltage(self):
        """Get the current voltage."""
        return self._voltage

After briefly looking over your code, the main difference seems to be that all of the classes you modified now unnecessarily call the corresponding setter whether or not it is necessary. This was done by deleting the private variable (starting with _) and assigning to a public one instead (which has the same name as the @property decorated function).

For example, I took two classes, Ganalytics and SubscriptionTracking and added print statements after each setter.

  • GanalyticsNEW and SubscriptionTrackingNEW are your classes.
  • GanalyticsORIGINAL and SubscriptionTrackingORIGINAL are the original classes.
>>> g = GanalyticsNEW()
enable setter
utm_source setter
utm_medium setter
utm_term setter
utm_content setter
utm_campaign setter
>>> g2 = GanalyticsORIGINAL()
>>> s = SubscriptionTrackingNEW()
enable setter
text setter
html setter
substitution_tag setter
>>> s2 = SubscriptionTrackingORIGINAL()

Of course, this happens only when each class falls back on the default None arguments but, other than lines of code reduced, I'm curious as to how this is an improvement?

@@ -1,9 +1,7 @@
class Category(object):

def __init__(self, name=None):
self._name = None
if name is not None:
self._name = name
Copy link
Contributor

Choose a reason for hiding this comment

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

# Shouldn't this...
self._name = name

# be this?
self.name = 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.

Will fix in next push.

Copy link
Contributor

Choose a reason for hiding this comment

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

@gabrielkrell ,

You deleted this code already as well...

self._groups_to_display = None

if group_id is not None:
self._group_id = group_id
Copy link
Contributor

Choose a reason for hiding this comment

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

# Shouldn't this...
self._group_id = group_id

# be this?
self.group_id = group_id

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the catch - will fix typo in next commit.

Copy link
Contributor

Choose a reason for hiding this comment

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

@gabrielkrell ,

I'm not sure how you are going to do this since you deleted this code already...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh shoot - I thought that my replacement code had the typo, not the original. Github's not too great on mobile but I'll poke at it soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah so with this and the other changes I kept the typo - my new code should use the setter and not assign directly to the private variables. Oops!

self._group_id = group_id

if groups_to_display is not None:
self._groups_to_display = groups_to_display
Copy link
Contributor

Choose a reason for hiding this comment

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

# Shouldn't this...
self._groups_to_display = groups_to_display

# be this?
self.groups_to_display = groups_to_display

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix in next push.

Copy link
Contributor

Choose a reason for hiding this comment

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

@gabrielkrell ,

You deleted this code already too...

@gabrielkrell
Copy link
Contributor Author

gabrielkrell commented Oct 31, 2017

Hey @delirious-lettuce, thanks for the detailed code review. I'll push the changes you pointed out once I get a little time.

Calling the setters on __init__ is intentional behavior. Reducing line count is the primary goal, but I think it makes sense for the setters to handle all property changes, even those as simple as adding a None.

However, I hadn't seen the docs example you showed, and I found more examples elsewhere. Upon further looking I can't find any that show it "my way", but neither can I find anything that explains why direct access is used instead of the setter. (I also couldn't find a PEP that introduced properties, so my Google-fu needs work.) I'm happy to close this if it's not best practice, but I'm curious about why direct access is considered better here. I know there's overhead in calling setters, but performance doesn't seem to be a concern since we have stub getters and setters in places where direct access could be used. In my view it makes things simpler with a minor performance penalty.

@delirious-lettuce
Copy link
Contributor

delirious-lettuce commented Oct 31, 2017

@gabrielkrell ,

I don't work for SendGrid or have any power over this PR, I was just reading through different PRs and became curious when I read yours. You are free to ignore my "review"!

That said, the main reason I became interested in your PR was that it was written differently than other code using @property that I was familiar with. I didn't know if that was good or bad so I just played around with it for a bit and that was the main difference that I saw (unnecessarily calling setters). I'm always looking to improve so I thought I would ask you in case there was something else I was missing that made it advantageous to write it that way.

@gabrielkrell
Copy link
Contributor Author

@delirious-lettuce No, no worries! I appreciate the pair of eyes (plus Matt and Elmer are pretty overworked right now from Hacktoberfest). You're definitely right about standard practice, I suppose I'd never noticed. It still feels like the right thing to do to me, but I don't like going against convention. If someone else could elaborate a bit on why examples use direct access it'd be awesome - maybe we can let this sit until then?

IMO, we probably shouldn't be using properties at all, since there's no error checking or special behavior in most places and the whole point of properties is that you can put them in as necessary without changing the interface. Without them we'd have the benefits of both approaches. But that's kind of beside the point since it would be too much trouble to remove them, we'd lose docstrings, etc.

Here's how I'd sum up pros/cons:

Pros:

  • Less conditionals: reduce mental complexity, flat is better than nested, blah blah
  • Smaller line count: make codeclimate happier (marginal)
  • IMO it makes sense for the setters to handle all changes.
    • Fails early: setter should always allow default value as an input

Cons:

  • More explicit (but this is an argument against setters in general too)
  • Unusual: python docs, standard implementations do it the old way.
    • Why? More info useful.
  • Performance penalty from calling setters in __init__ (marginal)

Ultimately though if it's unexpected enough to make you do a double-take I think that overrides whatever subjective simplicity it brings. I'll leave it up for now to get more opinions but my guess is it'll get closed.

@delirious-lettuce
Copy link
Contributor

delirious-lettuce commented Oct 31, 2017

@gabrielkrell ,

If someone else could elaborate a bit on why examples use direct access it'd be awesome - maybe we can let this sit until then?

Sure, no problem.

IMO, we probably shouldn't be using properties at all, since there's no error checking or special behavior in most places and the whole point of properties is that you can put them in as necessary without changing the interface. Without them we'd have the benefits of both approaches. But that's kind of beside the point since it would be too much trouble to remove them, we'd lose docstrings, etc.

Ya, you're right there isn't anything special in the getters/setters yet but I guess different functionality could be easily added in the future (if necessary). And yes, it also seems like a bit much to be removing them at this point to.

Performance penalty from calling setters in init (marginal)

I didn't want to make it seem like this was a big deal or anything (I didn't do any profiling to measure the difference), it was just that I couldn't find a lot of differences between the two different versions.

Ultimately though if it's unexpected enough to make you do a double-take I think that overrides whatever subjective simplicity it brings. I'll leave it up for now to get more opinions but my guess is it'll get closed.

I don't think I have enough experience to have that decision rest solely on my opinion of how it was written. As I've said, I was really just curious that this could be a better way to write it and was trying to figure out why. I'm still learning new things about Python on an almost daily basis and reading other developers code is one of the best ways that I've found to improve my own skills.

Thanks for your time!

@gabrielkrell
Copy link
Contributor Author

Thanks @delirious-lettuce! I learned something new as well :)

@mbernier
Copy link
Contributor

mbernier commented Nov 1, 2017

I am also of the mind that having setters and getters is ideal, because you can defer any future responsibility for modifying the parameters in the same place. this keeps the code simpler, when you're debugging - whereas having a direct access in one place with logic for how to set that param (defaulting, any ETL, or other modifications) and then also having a setter/getter that might have to do that seems redundant.

Having said all of that, it always "feels wrong" when making a basic class that is really just a clean way to organize and access parameters because it is so redundant.

I will defer to whatever the master, @thinkingserious, says here - my personal preference, verbose as it is, would be to make it a standard to have the getters and setters - which forces the issue for future code, as insurance against having parameters act one way on init and another on obj.param. This also has the impact of simplifying testing.

@mbernier
Copy link
Contributor

mbernier commented Nov 1, 2017

I also feel the need to add - THANK YOU both @gabrielkrell and @delirious-lettuce for having a great conversation about this here. I was able to quickly join the conversation and throw my $.02 in, whether it was helpful or not is up for debate - but you saved me TONS of time and provided great information that we can refer to in the future. I really you both and your time spent here.

@delirious-lettuce
Copy link
Contributor

delirious-lettuce commented Nov 1, 2017

@mbernier ,

Thanks for the input, that makes a lot of sense!

EDIT:

I've been having fun trying to do some reviews on these repos!

@mbernier
Copy link
Contributor

mbernier commented Nov 1, 2017

@delirious-lettuce it is a tough call - on one hand, we are actually asking quite a bit - "Write all this seemingly redundant code to save what is likely 2 lines of duplication". In the future though, we can easily say "Add this logic to the getter on this object" and there is no confusion about what we are saying in the issue or where the logic should be.

For a project with, as of this month, hundreds of contributors - I can definitely see the value in being explicit.

@delirious-lettuce
Copy link
Contributor

@mbernier ,

For a project with, as of this month, hundreds of contributors - I can definitely see the value in being explicit.

I would agree 100% (but my vote doesn't really matter haha)

Thanks to delirious-lettuce - these were in the original code but should have been caught here.  *Probably* don't want to bypass the setters.
@@ -1,5 +1,6 @@
class TrackingSettings(object):
"""Settings to track how recipients interact with your email."""

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Accidental change in merge commit - added a newline between class docstring and __init__ in several places, but this is consistent with the rest of the code (and probably some PEP rule).

@mbernier
Copy link
Contributor

mbernier commented Nov 2, 2017

@gabrielkrell

We have not been able to merge your Pull Request, but because you are awesome - we wanted to make sure you could still get a SendGrid Hacktoberfest shirt.

Please go fill out our swag form before Nov 5th and we will send the shirt! (We know that you might have tried this before and it didn’t work, sorry about that!)

You have till Nov 5th to fill out this form in order to get the Hacktoberfest shirt!

Thank you for contributing during Hacktoberfest! We hope to see you in the repos soon! Just so you know, we always give away a SendGrid shirt for your first ever non-Hacktoberfest PR that gets merged.

@mbernier mbernier merged commit a403813 into sendgrid:master Nov 11, 2017
@thinkingserious
Copy link
Contributor

Hello @gabrielkrell,

Thanks again for the PR!

We appreciate your contribution and look forward to continued collaboration. Thanks!

Team SendGrid DX

@mbernier
Copy link
Contributor

Hey @gabrielkrell - wanna chat as a contributor?

You can grab a time on my calendar

Will be great to catch up :)

@gabrielkrell
Copy link
Contributor Author

@mbernier I'd love to - sorry for taking so long. I grabbed a slot Tuesday PM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
difficulty: medium fix is medium in difficulty status: code review request requesting a community code review or review from Twilio
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants