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 a Settings object to advance Twine having a Real API #365

Merged
merged 6 commits into from
May 17, 2018

Conversation

sigmavirus24
Copy link
Member

This begins the work described in #361 and is structured as 3 distinct commits:

  • c7b1f61 adds a helper that enables us to make certain elements of our API reject positional arguments. This is a shim designed to help us provide an API that forces users to communicate to future maintainers what they're doing.

  • cfdb15f adds the actual Settings object that will become part of our public API and can be used to replace the mess of parameters we're using in our commands

  • 3ced656 actually begins using our Settings object internally in our commands to start the dog-fooding process.

I'm going to leave in-line comments as well to point out things that are interesting/potentially controversial.

This allows us to design the APIs so that they only take keyword
arguments to make it easier and more explicit for users.

Refs #361
This adds the Settings object described in our API specification so
folks can get a feeling for how it will work and what it will be
responsible for.

Refs #361
Let's start internally using our new API and dogfood it. This will allow
us to start understanding the impact of this new API.

Refs #361
Copy link
Member

@theacodes theacodes left a comment

Choose a reason for hiding this comment

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

Looks mostly fine. :)

if repository_url.startswith((repository.LEGACY_PYPI,
repository.LEGACY_TEST_PYPI)):
raise exceptions.UploadToDeprecatedPyPIDetected(
"You're trying to upload to the legacy PyPI site '{0}'. "
Copy link
Member

Choose a reason for hiding this comment

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

Since this error message is always the same, why not move it into the UploadToDeprecatedPyPIDetected 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.

That's a great idea! I can do that in this PR if you'd like or move it to another.

Copy link
Member

Choose a reason for hiding this comment

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

Since you're already touching this code, totally fine to do it here.

Copy link
Member Author

@sigmavirus24 sigmavirus24 May 17, 2018

Choose a reason for hiding this comment

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

✔️ done


def test_settings_transforms_config(monkeypatch):
"""Verify that the settings object transforms the passed in options."""
replaced_get_repository_from_config = pretend.call_recorder(
Copy link
Member

Choose a reason for hiding this comment

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

This amount of patching here makes me a little nervous. If we change any interfaces in settings.utils this test likely not fail, as the stubs here capture the interface at the time the tests were written. Can we accomplish this with less stubbing?

Copy link
Member Author

Choose a reason for hiding this comment

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

It bothered me as I was writing it. Part of this ties into the idea that Settings.__init__ does validation and other logic on top of initialization. This is why I was leaning more towards having an object that builds a Settings object. That would be simpler to avoid this altogether. At the same time, the proposal in #362 could also make this simpler to test without all of this obnoxious patching.

I was trying to avoid hitting stdin or the disk, in this case, but I'm also happy to use the method we have elsewhere of writing a temporary file with pypirc config in it and passing that to avoid having to do the stubbing. Let me know what you think.

Copy link
Member

Choose a reason for hiding this comment

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

If you're planning on revisiting this test later on, then LGTM. Otherwise, yeah, I would prefer to just turn it into an "integration" test. Also I know I'm a bit picky about tests so feel free to disregard.

(shameless self-promotion: I wrote a blog about some of this stuff - use real collaborators when possible, and if you can't, be real careful about how you mock/stub).

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm picky too. I also tend to try to fit that into the existing strategy of the test suite if I'm taking over someone's project.

Copy link
Member

Choose a reason for hiding this comment

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

We're on the same page there. :)

Copy link
Member Author

Choose a reason for hiding this comment

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

✔️ Done

sign_with=None, config_file=pypirc, skip_existing=False,
repository_url=None, verbose=None,
)
upload_settings = settings.Settings(
Copy link
Member Author

Choose a reason for hiding this comment

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

This test shows that we've essentially made our validation of configuration settings/values more common than just the upload command. Future commits should rewrite this test and move it to tests/test_settings.py.

@@ -178,12 +180,7 @@ def none_upload(*args, **kwargs):
"TWINE_CERT": "/foo/bar.crt"}
with helpers.set_env(**testenv):
cli.dispatch(["upload", "path/to/file"])
cli.dispatch(["upload", "path/to/file"])
Copy link
Member Author

Choose a reason for hiding this comment

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

With much of the work separated out into the Settings object which is created and managed outside of the upload.upload function, our tests will error on this call since the Settings object otherwise attempts to retrieve the missing values.


# Call the register function with the args from the command line
register(**vars(args))
register(register_settings, args.package)
Copy link
Member Author

Choose a reason for hiding this comment

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

Our Settings class distinctly only cares about what it cares about. Extra arguments need to be handled separately by the user of the object. This is something I could see us revising to allow for a unified usage.


args = parser.parse_args(args)
upload_settings = settings.Settings.from_argparse(args)
Copy link
Member Author

Choose a reason for hiding this comment

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

The change to the environment variable test in tests/test_upload.py stems from this. If instead we created the Settings object inside the upload function the one invocation would not error but the first would fail (since we'd never call Settings.from_argparse. In short, this indicates to me that the environment var test needs to be rewritten to accommodate both of these cases and should be moved to tests/test_settings.py

@@ -13,33 +14,52 @@
# limitations under the License.


class RedirectDetected(Exception):
class TwineException(Exception):
Copy link
Member Author

Choose a reason for hiding this comment

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

As we're working towards an API. I've updated all of Twine's exceptions to have a single root in TwineException. This makes API users' lives easier. They can use a catch-all

try:
    ...
except twine.exceptions.TwineException:
    ...

Instead of trying to catch all of them. This can be removed from this PR if desired.

self.config_file = config_file
self.comment = comment
self.skip_existing = skip_existing
self._handle_repository_options(
Copy link
Member Author

Choose a reason for hiding this comment

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

I think these methods will be the most controversial part of this.

On the one hand, I can imagine having a separate object, e.g., SettingsBuilder, that does this work and then makes Settings into a mostly data class that stores the validated/correct configuration and does the other nice things (like checking repository URLs and creating a new repository). I went with the simpler/thinner implementation to start with given the lack of feedback on the API design document, though.


def create_repository(self):
"""Create a new repository for uploading."""
repo = repository.Repository(
Copy link
Member Author

Choose a reason for hiding this comment

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

This logic was already being repeated, so I pulled it out to here. The same goes for the logic in check_repository_url.

@codecov
Copy link

codecov bot commented May 17, 2018

Codecov Report

Merging #365 into master will increase coverage by 4.82%.
The diff coverage is 78.09%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #365      +/-   ##
==========================================
+ Coverage   68.84%   73.66%   +4.82%     
==========================================
  Files          12       13       +1     
  Lines         597      638      +41     
  Branches       95       97       +2     
==========================================
+ Hits          411      470      +59     
+ Misses        158      142      -16     
+ Partials       28       26       -2
Impacted Files Coverage Δ
twine/commands/register.py 0% <0%> (ø) ⬆️
twine/exceptions.py 100% <100%> (ø) ⬆️
twine/utils.py 84.37% <100%> (+3.67%) ⬆️
twine/commands/upload.py 65.62% <40%> (-0.26%) ⬇️
twine/settings.py 91.22% <91.22%> (ø)

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 2ab1a60...b341906. Read the comment docs.

Since the message is always the same, let's keep it with the class to
avoid any confusion.
@sigmavirus24
Copy link
Member Author

@theacodes did you want to take another look at this?

Copy link
Member

@theacodes theacodes left a comment

Choose a reason for hiding this comment

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

LGTM!

@sigmavirus24 sigmavirus24 merged commit f2695b0 into master May 17, 2018
@sigmavirus24 sigmavirus24 deleted the settings-object branch May 17, 2018 19:33
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