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

Reach 100% code coverage #13

Merged
merged 23 commits into from
Feb 14, 2017
Merged

Reach 100% code coverage #13

merged 23 commits into from
Feb 14, 2017

Conversation

jamadden
Copy link
Member

Mostly this means new tests. Sometimes its means "no cover" markers. In a few cases it means using a shorter, more declarative way to write the code (e.g., @total_ordering or ABCMeta) or unifying python2/python 3 branches as much as possible. Sometimes we got lucky and could drop code meant for 2.6 or earlier.

There are a few genuine bugfixes in here that were revealed by getting the coverage up.

I tried to limit the use of 'no cover' as much as possible. There are a few deeply nested functions that made it difficult to avoid, though.

This replaces #12.

Also enable pip caching on CI
This helps with our coverage numbers.

Under Python 2, we also start using the C implementation, and avoid
some unnecessary encode/decode cycles, so we might be a tiny bit
faster.
The str values of the exceptions.
Also when it raises an error, preserve the traceback if we're
re-raising. This was invaluable in debugging this.
Had to use one 'no cover' sadly.
…entedError. This helps coverage numbers.
Notable:

- float conversion now correctly handles bytes and unicode prohibited
  values on all versions.
- remove a duplicate test function from test_logger.
- remove test support in test_logger for < 2.7
- Fix StartupHandler on Python 3.
- SectionType has a few no-covers for exception cases that I didn't
  feel like mocking up all the conditions for.

- Make the type objects, well, objects. They had a ``__metaclass__``
  statement that I think did the same thing on Python 2.
There are unfortunately a few no-covers because of very deeply nested
functions that are difficult to mock around.

There is at least one genuine bug fix.
…ecause I couldn't come up with counter examples.
@coveralls
Copy link

coveralls commented Feb 11, 2017

Coverage Status

Changes Unknown when pulling cc07327 on coverage-100 into ** on master**.

@tseaver
Copy link
Member

tseaver commented Feb 12, 2017

@jamadden Now that it is at 100%, can you make coveralls not spew into the conversation itself? I hate getting emails from bots. :)

@jamadden
Copy link
Member Author

I sure can and did. It's on by default, and the badge sure is pretty to look at, but it probably doesn't add much value. And there was that recent bug where it was commenting for every Travis build in the matrix which was really annoying; fortunately that seems to have been fixed.

@mgedmin
Copy link
Member

mgedmin commented Feb 13, 2017

Making the tests fail when coverage drops below 100% (as @tseaver likes to do, which I greatly admire) is a better way to maintain test coverage than those Coveralls comments that annoy everyone and that nobody ever reads.

@jamadden
Copy link
Member Author

PRs are set to fail in that case.

Copy link
Member

@mgedmin mgedmin left a comment

Choose a reason for hiding this comment

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

LGTM with minor comments.

.coveragerc Outdated
# things. Omit the main methods of test files.
exclude_lines =
pragma: no cover
pragma no cover
Copy link
Member

Choose a reason for hiding this comment

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

This is very minor but it bugs me that we have two ways of spelling that pragma.

Copy link
Member Author

@jamadden jamadden Feb 13, 2017

Choose a reason for hiding this comment

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

I guess there are no occurrences of 'pragma no cover' anymore so that can go away.


exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
Copy link
Member

Choose a reason for hiding this comment

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

I kind of hate the way this is split into multiple lines. I'd prefer

    exec_("""
def reraise(tp, value, tb=None):
    raise tp, value, tb
""")

or

    exec_("def reraise(tp, value, tb=None): raise tp, value, tb")

Copy link
Member Author

Choose a reason for hiding this comment

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

For better or worse, that code is copied verbatim from six and various other zopefoundation packages that have done the same.

@@ -175,7 +170,8 @@ def replace(self, text):
raise

def error(self, message):
raise ZConfig.ConfigurationSyntaxError(message, self.url, self.lineno)
v = ZConfig.ConfigurationSyntaxError(message, self.url, self.lineno)
reraise(type(v), v, sys.exc_info()[2])
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand the purpose of this change. Are you replacing the exception class and message but keeping the original traceback?

Perhaps define a helper function raise_with_same_tb(exc), because I see a lot of places in the diff that do this sort of thing with new temporary variables (all named differently) and type(...).

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this is to keep the original traceback. There are lots of places in the code that do try:...except: self.error(). Debugging those was a nightmare without keeping the original traceback.

The simplest textual change was to change the (relatively few) error functions instead of changing all the callers.

if is_str:
if v.lower() in ["inf", "-inf", "nan"]:
if v.lower() in (u"inf", u"-inf", u"nan", b'inf', b'-inf', b'nan'):
raise ValueError(repr(v) + " is not a portable float representation")
Copy link
Member

Choose a reason for hiding this comment

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

float() was fixed in Python 2.6 to interpret inf and nan in a portable fashion, so this check can be safely removed as ZConfig no longer needs to protect the users from this pitfall.

That would basically simplify this whole function into return float(v), probably making it unnecessary -- just use float() wherever float_conversion() is currently being called.

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 not intimately familiar with the history there, but the linked docs still contain the phrase "When passing in a string, values for NaN and Infinity may be returned, depending on the underlying C library" (emphasis mine). It does say that float represents things a certain way, but it's not clear to me that it commits to parsing things a certain way.

I'd be happy to see this simplified, and that could arguably be a part of the changes for this PR. It's just not entirely clear to me its safe to do so.

Copy link
Member

Choose a reason for hiding this comment

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

The "depending on the underlying C library" bit remains from the earlier text, which warned that the specific inf/nan spellings did depend on the C library: http://svn.python.org/view/python/trunk/Doc/library/functions.rst?r1=59558&r2=59557&pathrev=59558. I wonder why it wasn't removed.

The "Float accepts the strings nan, inf and -inf for NaN and positive or negative infinity. The case and a leading + are ignored as well as a leading - is ignored for NaN." part is about the parsing. There are stdlib tests for this too.

The "Float always represents NaN and infinity as nan, inf or -inf." part is about the repr().

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, cool! Thanks for the pointers. Should this change be a part of this admittedly large PR, or should it wait? ( I am willing to create follow up issues for this and other things that come out of this discussion. )

Copy link
Member

Choose a reason for hiding this comment

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

Separate PR, I think, to keep things simple.

@@ -228,6 +220,8 @@ def __init__(self, name, sectiontype, minOccurs, maxOccurs, handler,
# handler - handler name called when value(s) must take effect,
# or None
# attribute - name of the attribute on the SectionValue object
assert maxOccurs is not None
# because we compare it to 1 which is a Py3 error
Copy link
Member

Choose a reason for hiding this comment

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

It is strange then that the constructor explicitly allows maxOccurs to be set to None, and that, e.g., def ismulti is comparing self.maxOccurs > 1 unconditionally.

Python 2.x returns False for None > 1. I cannot say whether that makes sense for ismulti or here.

Copy link
Member

Choose a reason for hiding this comment

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

BTW getdefault also unconditionally compares self.maxOccurs > 1.

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 the weirdness is partially because this is a subclass of an abstract class that allows subclasses to define the constructors differently (which they do, passing constants for some of the arguments). That comparison works on Py2, but fails on Py3.

Once we do the assert in the constructor, getdefault doesn't matter anymore?

Copy link
Member

@mgedmin mgedmin Feb 13, 2017

Choose a reason for hiding this comment

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

Yeah, I think we should either fully support maxOccurs being None (maybe that is how you spell "unlimited"?) by adding code to make Python 3 handle this explicitly instead of failing, or we should require it to be not-None everywhere (i.e. remove the explicit "let it be None" case in the constructor and replace it with an assertion, or, better yet, an explicit ValueError).

I'm sorry that I don't currently have the time to investigate what maxOccurs is used for!

'malformed section start',
self.loadtext, '<section')

# ConfigLoader.endSection raises this and its recaught and changed to a
Copy link
Member

Choose a reason for hiding this comment

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

Typo: its -> it's

@@ -93,14 +93,13 @@ def test_datatype_float(self):

# These are not portable representations; make sure they are
# disallowed everywhere for consistency.
Copy link
Member

Choose a reason for hiding this comment

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

As I mentioned above, this comment is no longer true since Python 2.6.

check(":80", AF_INET, (defhost, 80))

convert = self.types.get('socket-connection-address')
check(":80", AF_INET, ("127.0.0.1", 80))
Copy link
Member

Choose a reason for hiding this comment

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

That is a strange amount of blank spaces between function arguments. PEP-8 frowns on this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed. But that matches the way this is called in the rest of the function.

Copy link
Contributor

Choose a reason for hiding this comment

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

I can only imagine I was thinking that looking like a table would make this more readable. I wouldn't make the same mistake now. (I certainly hope.)

suite.addTest(unittest.makeSuite(AbstractTypeTestCase))
suite.addTest(unittest.makeSuite(SectionTypeTestCase))
suite.addTest(unittest.makeSuite(SchemaTypeTestCase))
return suite
Copy link
Member

Choose a reason for hiding this comment

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

Strongly suggest

def test_suite()
    return unittest.defaultTestLoader.loadTestsFromName(__name__)

unless you have abstract classes inheriting from unittest.TestCase that would preclude this?

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 haven't seen that idiom used anywhere in this codebase (or in any of the other zopefoundation projects I've touched so far). In fact, I wasn't actually aware of it.

I used this style because that's what was used in the rest of the code base. I'd be happy to see it change to a simpler style, but I submit that would belong in a separate PR that does the same for all the test modules.

Copy link
Member

Choose a reason for hiding this comment

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

I don't recall where I learned this trick, but I recall getting burnt a few times in Zopeland when I (or somebody else) wrote a new test class and then forgot the corresponding makeSuite() bit. Of course, measuring test coverage protects from that to some extent.

I also have doubts whether that refactoring belongs to this already-very-large PR (especially if we want to convert all the files in one go, rather opt for a more incremental approach and convert just those that are touched in this PR).

I have this habit where when I'm reviewing code I also tend to look for possible improvements in the surrounding codebase, and also for opportunities to share knowledge. But then I forget to state that that's what I'm doing, rather than asking for changes in this particular PR.

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 was burnt a ridiculous number of times on this very PR by that issue :/ it was quickly noticed and fixed, but still.

I'm happy to create a follow up issue. And I will be doing the same for other projects where I've dealt with this (relstorage).

return doctest.DocFileSuite("schemaless.txt", package="ZConfig")
suite = unittest.makeSuite(TestSection)
suite.addTest(doctest.DocFileSuite("schemaless.txt", package="ZConfig"))
return suite
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 switch to using defaultTestLoader everywhere, a nice way to mix it with doctests is

def test_suite():
    return unittest.TestSuite([
        unittest.defaultTestLoader.loadTestsFromName(__name__),
        doctest.DocFileSuite(...),
    ])

# Python 2
from io import BytesIO as _NStringIO

NStringIO = _NStringIO
Copy link
Member

Choose a reason for hiding this comment

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

Why the alias?

# Python 3 support.
import urllib.request as urllib2

urllib2 = urllib2
Copy link
Member

Choose a reason for hiding this comment

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

Why rebind the name?

@@ -155,7 +157,7 @@ def get_section_info(self, type, name):
bk = self.basic_key(s, pos)
if name and self._normalize_case(s) == name:
L.append((optpath[1:], val, pos))
elif bk == type:
elif bk == type: # pragma: no cover
Copy link
Member

Choose a reason for hiding this comment

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

Ugh, the shadowing of the type built-in is really ugly: I spent more than a bit of time trying to work out how the code worked. I wonder if we could rename the argument type_?

Copy link
Member Author

@jamadden jamadden Feb 13, 2017

Choose a reason for hiding this comment

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

There would be a lot or renames to do throughout the codebase. type (among others) is commonly shadowed; I think this code predates new-style classes and type as a builtin.

I'd be happy to see that done, but I submit that it belongs in another PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

type has been a built-in for a very long time (since before I discovered Python at version 1.2). I've never been particularly averse to shadowing it, though.

Copy link
Member

Choose a reason for hiding this comment

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

FWIW when I was reviewing this, I wanted to ask why this comparison wasn't using if bk is type to compare with the type builtin. Then I thought a bit more and decided maybe type was a local variable shadowing the builtin.

(Also, given that my Vim syntax-highlights type in a different color from regular variables makes me want to use type_ instead of shadowing.)

Copy link
Member

Choose a reason for hiding this comment

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

@mgedmin that was exactly what I thought when reading that diff (it took me a couple of minutes to stumble on the shadowing).

SysLogHandler = SysLogHandler
HTTPHandler = HTTPHandler
SMTPHandler = SMTPHandler
Win32EventLogHandler = Win32EventLogHandler
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand what "export these" means, or what that has to do with re-binding the name.

On another note: couldn't handlers.py just import them from logging.handlers?

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 applies to this comment and two of the previous ones:

linters like flake8 and pylint (I run/ran both during this process) complain (rightfully) about unused imports. The way to fix that without adding #noqa or what have you is to use the import, and the simplest way to do that is to rebind it to the same name. This declarative statement seems reasonably clear to me that this name is a public part of the module definition, as opposed to a side-effect of the import.

In this particular case, it seemed important to leave the imports as they are for backwards compatibility.

Copy link
Member

Choose a reason for hiding this comment

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

Let's just hope linters won't decide to start warning about "useless assignment X = X" in the future ;)

Copy link
Member

Choose a reason for hiding this comment

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

  • I find the reassignment useless, and would prefer a comment like #noqa API to make the intent clearer.
  • I don't think we need to support backward-compatibility for re-exports of stdlib modules.

except ImportError:
# Python 3 support.
import io as StringIO
from ZConfig._compat import NStringIO as StringIO
Copy link
Member

Choose a reason for hiding this comment

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

Why not leave the name NStringIO, since we have to munge all the callers anyway?

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 seemed like a slightly smaller change, but I admit it wasn't fully thought through. There was some evolution here. It can still change if desired.

@jamadden
Copy link
Member Author

I created new issues for the things that have come out in the comments. Is this mergeable?

@mgedmin
Copy link
Member

mgedmin commented Feb 14, 2017

You still have a green check from me :)

The only thing I still find kind of distasteful in the new changes is this:

  • Perhaps define a helper function raise_with_same_tb(exc), because I see a lot of places in the diff that do this sort of thing with new temporary exception variables (all named differently) and reraise(type(exc), exc, sys.exc_info()[2]).

@jamadden
Copy link
Member Author

Ah, now I understand what you mean by that. That should be easy enough to change and reduce the size of the PR a bit. I'll do that.

@jamadden jamadden merged commit 010cb2b into master Feb 14, 2017
@jamadden jamadden deleted the coverage-100 branch February 14, 2017 14:48
@mgedmin
Copy link
Member

mgedmin commented Feb 14, 2017

Thank you, looks great!

jamadden added a commit that referenced this pull request Feb 14, 2017
Remove 'try/except NameError' around uses of __file__

See #13 (comment)
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.

5 participants