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

Added --format #23

Merged
merged 20 commits into from
Oct 30, 2018
Merged

Added --format #23

merged 20 commits into from
Oct 30, 2018

Conversation

erayerdin
Copy link
Contributor

Related to #20

Added --format flag (for short, -f) to cli args. --json is kept for convenience.

  -j, --json            Output JSON. Shortcut for "-f json".
  -f FORMAT, --format FORMAT
                        The format of output. Supported: json, markdown.
                        Default is "markdown".

The default is markdown. The cli tool also provides a UserWarning if given format is not supported and will inform user that it will fallback to default, which is, again, markdown. An example below:

$ pypistats recent pyairmore -f mp3  # siriously, wth?
foo/bar/pypistats/pypistats/cli.py:132: UserWarning: Unknown format: mp3. Using "markdown".
  warnings.warn(f'Unknown format: {args.format}. Using "markdown".')
| last_day | last_month | last_week |
|---------:|-----------:|----------:|
|        3 |        151 |        33 |

Other Minor Changes

  • Verbose flake8 and black output on Travis
  • flake8 and black now checks pypistats/ and tests/ directory instead of whole project which makes it a little bit faster (especially black).
  • Added tests_require to setup.py.

The only problem is that the code I provided was not tested by pytest since it is not quite possible to both mock HTTP requests and test cli at the same time. The codebase was tested by hand 😢. This will probably drop code coverage by ~5%. If you are strict about testing, I can find a way around (then inform me, don't merge this PR).

If you'd like to test for yourself by hand, you can clone forked copy and checkout feat/format-arg branch.

@codecov
Copy link

codecov bot commented Oct 29, 2018

Codecov Report

Merging #23 into master will increase coverage by 3.52%.
The diff coverage is 92.3%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master     #23      +/-   ##
=========================================
+ Coverage   78.97%   82.5%   +3.52%     
=========================================
  Files           3       3              
  Lines         195     200       +5     
=========================================
+ Hits          154     165      +11     
+ Misses         41      35       -6
Impacted Files Coverage Δ
pypistats/cli.py 83.78% <92.3%> (+9.87%) ⬆️

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 b11a8c7...a62484a. Read the comment docs.

@erayerdin
Copy link
Contributor Author

erayerdin commented Oct 29, 2018 via email

@hugovk
Copy link
Owner

hugovk commented Oct 29, 2018

This block is repeated five times, and could go into a function which would reduce the coverage drop, and would be more easily tested:

    if args.json or args.format == "json":
        output = "json"
    elif args.format == "markdown":
        output = "table"
    else:
        import warnings

        warnings.warn(f'Unknown format: {args.format}. Using "markdown".')
        output = "table"

That may sort the coverage drop, otherwise some options for testing is using requests_mock like in 4459141, or perhaps even mocking pypistats.recent.

Copy link
Owner

@hugovk hugovk left a comment

Choose a reason for hiding this comment

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

A few other notes :)

pypistats/cli.py Outdated
elif args.format == "markdown":
output = "table"
else:
import warnings
Copy link
Owner

Choose a reason for hiding this comment

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

Let's move the import to the top and use isort to put them in order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright.

.travis.yml Outdated
- flake8 --statistics --count
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then black --check --diff .; fi
- flake8 --statistics --count --verbose pypistats/ tests/
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then black --check --diff --verbose pypistats/ tests/; fi
Copy link
Owner

Choose a reason for hiding this comment

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

Is there a reason to specify these dirs and not run in .? We miss setup.py like this.

Also, let's leave out --verbose so the output is a cleaner.

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 didn't think about setup.py, sorry.

I put --verbose in case of referring Travis build logs via URL if anyone wants to contribute to show contributors what the problem is.

Copy link
Owner

Choose a reason for hiding this comment

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

Sorry, just read your PR message fully now, and it's done for speed.

The CI took 3.13s to run Black with this PR:

https://travis-ci.org/hugovk/pypistats/jobs/447664623#L640

and 4.54s with master:

https://travis-ci.org/hugovk/pypistats/jobs/447539626#L580

I think that's fine.

Copy link
Owner

Choose a reason for hiding this comment

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

If there's a flake8 error, it lists them in the logs wihout --verbose.

And if Black errors, --diff shows what should be have been done.

I think the verbose stuff is probably a bit distracting.

pypistats/cli.py Outdated Show resolved Hide resolved
setup.py Outdated Show resolved Hide resolved
@@ -15,6 +15,8 @@

BASE_URL = "https://pypistats.org/api/"

FORMATS = ("json", "markdown") # only used for printing to terminal
Copy link
Owner

Choose a reason for hiding this comment

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

Would this be better kept internal to cli.py?

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'll do it.

@erayerdin
Copy link
Contributor Author

This block is repeated five times, and could go into a function which would reduce the coverage drop...

Yes, that's a good idea.


...some options for testing is using requests_mock like in 4459141, or perhaps even mocking pypistats.recent.

I was just trying requests_mock. On the other hand, I thought about two methods to test cli with requests_mock alongside.

Method 1: Using Subprocess

I can test via subprocess also using requests_mock.

Pros Cons
Can provide a basis for behavioral(ish?) tests for cli Since subprocess spawns another process, requests_mock will not be able to mock.
Because subprocess is testing cli output, it will not drastically change the coverage.

Method 2: Testing Functions under cli.py

I could test @subcommand decorated functions under cli.py.

Pros Cons
Simple enough Testing print is not possible.

What I'm Going to Do

Overall, I think, as you've said, I will move those repeating if-else parts to a protected/underscore function and see the coverage myself. Then choose a way to solve the problem.

And, also, will look into the problems you've mentioned above. :)

@erayerdin
Copy link
Contributor Author

Phew, finally.

Properly tested, covered. Dealt with minor issues you have mentioned.

Copy link
Owner

@hugovk hugovk left a comment

Choose a reason for hiding this comment

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

Looking good, a few more notes :)

pypistats/cli.py Show resolved Hide resolved
pypistats/cli.py Outdated
default="markdown",
choices=FORMATS,
help="The format of output.",
),
Copy link
Owner

Choose a reason for hiding this comment

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

Let's put all these repeated argument(...)s in an arg_format variable, like arg_start_date

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 do

pypistats/cli.py Outdated
"--format",
default="markdown",
choices=FORMATS,
help="The format of output.",
Copy link
Owner

Choose a reason for hiding this comment

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

Remove fullstop for single sentence:

Suggested change
help="The format of output.",
help="The format of output",

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 do

pypistats/cli.py Outdated
if args.mirrors in ["with", "without"]:
args.mirrors = args.mirrors == "with"

_format = _define_format(args)
Copy link
Owner

Choose a reason for hiding this comment

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

We can actually move all these calls down to a single args.format = _define_format(args) down in the else block at the end of the file, and then use output=_format here.

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 check

@hugovk
Copy link
Owner

hugovk commented Oct 29, 2018

One more thing, one of the two hardest things in computer science, and it's not cache invalidation. :)

First, we're still at version 0.y.z so I'm not too worried about backwards compatibility at this point, if it means getting it right.

So on the programmatic side, we have output which can be json or table:

print(pypistats.recent("pillow"))  # output="table"
print(pypistats.recent("pillow", output="table"))
print(pypistats.recent("pillow", output="json"))

Previously on the CLI side we had --json (which gave output=json) or a default (output=table) which was Markdown.

$ pypistats recent pillow         # output="table"
$ pypistats recent pillow --json  # output="json"

Now on the CLI side, we have --format which can be json or markdown. When the user makes a CLI call, we translate format -> output and markdown -> table.

$ pypistats recent pillow                    # output="table"
$ pypistats recent pillow --format markdown  # output="table"
$ pypistats recent pillow --json             # output="json"
$ pypistats recent pillow --format json      # output="json"

What do you think, shall we unify the terms? It would probably clearer if this tool isn't using different terms in its two interfaces (CLI and programmatic).

For example, use either output or format on both sides, and table or markdown on both sides:

$ pypistats recent pillow                    # output="markdown"
$ pypistats recent pillow --output markdown  # output="markdown"
$ pypistats recent pillow --json             # output="json"
$ pypistats recent pillow --output json      # output="json"
  • output has the benefit of not shadowing the builtin format (I'm also open to a third choice)
  • markdown is more specific than table especially when RST and HTML tables are added
  • and markdown is the default anyway.

Thoughts? (This could go in a separate PR.)

@erayerdin
Copy link
Contributor Author

erayerdin commented Oct 29, 2018

TL;DR

  • format is the way to go.
  • All hail markdown.
  • can provide contribution on a separate PR
  • All these are just my humble opinions. If you are okay, then so am I.

format vs. output

Shadowing Issue

format definitely shadows Python's built-in method. However, it naturally does not shadow if it is a property of an object. In our case in cli.py, we mostly use args.format (in my latest commit at least) rather than format alone. So, IMHO, it is not much of an issue.

On the programmatic side, *insert developer here* provides format as a method parameter, which clearly does not cause any shadowing. I mean:

pypistats.recent("pillow", format="json")  # if a developer does this
format(whatever)   # they can still use built-in `format` method

Terms

Yes, format and output are interchangeable terms, yet I think they sense different.

I do not know if it makes sense, but markdown, rst, html and json are formats since they have convenient rules. On the other hand, output, to me at least, is like a binary mimetype, like an Excel file or Word file, which use different formats like xmls and xhtmls and then zip the final product altogether. If we had plans to provide detailed Excel output on cli in order to create a file, which we probably do not for now, then we could use output right then and there.

Is semantics or terminology worth the hassle though? This is the issue. I think we should use the term format and update the codebase, which is hard but not a pain in the butt. I use Pycharm Professional. It has generous reformatting capabilities, which will make it easier and then run tests locally and test by hand to ensure anything works.

On markdown vs. table and Backwards Compability

...we're still at version 0.y.z so I'm not too worried about backwards compatibility at this point...

I actually wished to change it, but, you know, hesitated it since you might want to be comfortable in your terms. If backwards compatibility is not a huge deal now, I can deal with that. My two cents are on "markdown".

pypistats/cli.py Outdated
arg_daily = argument("-d", "--daily", action="store_true", help="Show daily downloads")
arg_format = argument(
"-f", "--format", default="markdown", choices=FORMATS, help="The format of output."
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
"-f", "--format", default="markdown", choices=FORMATS, help="The format of output."
"-f", "--format", default="markdown", choices=FORMATS, help="The format of output"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lol I was dead sure I have deleted it, seems like I didn't.

pypistats/cli.py Show resolved Hide resolved
@hugovk
Copy link
Owner

hugovk commented Oct 29, 2018

Yes, let's go for markdown (in another PR).

If this output was format, would it shadow the builtin?

def pypi_stats_api(
endpoint,
params=None,
output="table",
start_date=None,
end_date=None,
sort=True,
total=True,
):

if output == "json":
return json.dumps(res)

@erayerdin
Copy link
Contributor Author

If this output was format, would it shadow the builtin?

It shadows format in pypi_stats_api method's scope, but not out of it. In programmatic sense, it won't affect the developer who uses this library. In terms of our codebase, it might, depending on if we decide to use format inside pypi_stats_api method in the future.

If shadowing format inside inside pypi_status_api method is a significant issue, then we can keep output as is, throwing away format.

@hugovk
Copy link
Owner

hugovk commented Oct 29, 2018

Let's go with format and markdown. If we need to use the builtin in the future, we can always do something like del format. Or just use f-strings instead :)

@hugovk
Copy link
Owner

hugovk commented Oct 30, 2018

Thank you very much!

@hugovk hugovk merged commit 2b6e89e into hugovk:master Oct 30, 2018
@erayerdin erayerdin deleted the feat/format-arg branch October 31, 2018 17:34
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.

None yet

2 participants