Skip to content

Fixes #66: Adds support for SIDEBOARD_CONFIG_OVERRIDES env var#68

Merged
RobRuana merged 2 commits intomasterfrom
fix-66-sideboard-config-overrides
Mar 15, 2017
Merged

Fixes #66: Adds support for SIDEBOARD_CONFIG_OVERRIDES env var#68
RobRuana merged 2 commits intomasterfrom
fix-66-sideboard-config-overrides

Conversation

@RobRuana
Copy link
Copy Markdown
Contributor

I am like 90% sure I am forgetting something. So please look over this and ask questions before approving it.

@coveralls
Copy link
Copy Markdown

coveralls commented Mar 15, 2017

Coverage Status

Coverage increased (+0.3%) to 77.647% when pulling 6bc49e7 on fix-66-sideboard-config-overrides into aa40f7e on master.

Copy link
Copy Markdown
Member

@kitsuta kitsuta left a comment

Choose a reason for hiding this comment

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

This generally looks fine to me so I asked a bunch of dumb questions instead. :) I would like to see uniquify either return an ordered list or the docstring altered, though.

Comment thread sideboard/config.py Outdated

"""
seen = set()
return [i for i in x if i not in seen and not seen.add(i)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure I'm understanding this list comprehension, particularly the not seen.add(i). How does a conditional work with a function like that? I went ahead and tested this in the IDE and it seems to work fine, I just don't know how.

On a separate note, what DOESN'T work is trying to uniquify a list that's out of order. It will not, as the docstring implies, order the list for you. E.g.:

>>> x = ['e', 'b', 'a', 'c', 'a', 'd', 'a', 'e']
>>> seen = set()
>>> [i for i in x if i not in seen and not seen.add(i)]
['e', 'b', 'a', 'c', 'd']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On a separate note, what DOESN'T work is trying to uniquify a list that's out of order.

I think what Rob meant was that it would maintain the existing order rather than sort them. I've requested a clarification in the docstring.

I'm not sure I'm understanding this list comprehension, particularly the not seen.add(i).

The code is exactly equivalent to the following:

xs = []
for i in x:
    if i not in seen:
        xs.append(i)
    seen.add(i)
return xs

The tricky part about implementing that as a list comprehension is the fact that we have to add the elements to seen as we go. However, since seen.add(i) adds the current element to the set and always returns None no matter what, we can add it as a conditional in our list comprehension.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two suggestions about this function:

  1. You should probably call listify on the parameter just to be safe. I'm a big fan if listify :)

  2. Purely a style suggestion: I like the Haskell convention of using xs as the variable name for "a generic iterable" and x as a variable name for "a generic element". We use that convention elsewhere in Sideboard, and if you used it here then we'd say [x for x in listify(xs) if x not in seen and not seen.add(x)]. The main reason why I like that more than for i in x is that i is usually the variable name used for indexes, and I did a double take when first reading this function to make sure we were collecting elements rather than indexes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, I figured a possibility might be that the function was working as intended but the docstring just implied something different. Updating the docstring is fine.

That's crazy that you can do that with list comprehension.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A few notes about this function:

  1. It really belongs in sideboard.lib._util, but trying to import it from there causes a circular import error (even if I import it from inside a function).

  2. I usually use "ordered" to mean the the order of elements is relevant and preserved, and "sorted" to mean in ascending or descending order. That being said this docstring is totally ambiguous and needs a rewrite.

  3. It doesn't need to be a list comprehension at all, and would be clearer in long form as described above.

  4. In my original implementation I DID call listify on the argument! But then I thought, if someone passes in an argument that is not already "listy" we should probably error on that. For example, if someone accidentally passes in None, then we would return [None], which I don't think is what we intend to do. So, for my perspective, if that's really what someone wants, then they can listify the argument before passing it to uniquify.

  5. I used x as the parameter name because that's what we also use for listify and is_listy, but then I was like, "what variable am I gonna use for the items? Oh well, i it is!"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It really belongs in sideboard.lib._util, but trying to import it from there causes a circular import error (even if I import it from inside a function).

That makes sense because sideboard.lib assumes that sideboard.config has already been implemented. However, if we decide we want it to be importable in sideboard.lib then we could import it in sideboard/lib/__init__.py and then add it to that module's __all__. However, I'm not sure this is generally useful enough for us to put it there.

In my original implementation I DID call listify on the argument! But then I thought, if someone passes in an argument that is not already "listy" we should probably error on that. For example, if someone accidentally passes in None, then we would return [None], which I don't think is what we intend to do. So, for my perspective, if that's really what someone wants, then they can listify the argument before passing it to uniquify.

Reasonable! In that case I suggest importing sideboard.lib.is_listy adding a line like

assert is_listy(xs), 'uniquify requires a listy argument'

I used x as the parameter name because that's what we also use for listify and is_listy, but then I was like, "what variable am I gonna use for the items? Oh well, i it is!"

Also reasonable. The reason why we use x for listify and is_listy is because we're not sure whether it's already an iterable or not, so I thought x made more sense than xs there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good luck importing is_listy into config.py!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm actually a fan of list comprehension, so I don't think you need to explode it. It's pretty clear what this function does -- I just wanted to know exactly how it was working. It would be different if this was part of a more complex function, but because it's in its own function it is totally fine as-is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh right! Fair enough, probably not worth the effort!

Comment thread sideboard/config.py Outdated
config_paths = []
for config_path in uniquify([s.strip() for s in config_overrides.split(';')]):
config_paths.append(config_path)
m = _defaults_re.match(config_path)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This (specifically _defaults_re) is some crazy regexp but I tested it in the IDE and it seems to work.

Comment thread sideboard/config.py
files as "root" and "module_root" and are available for interpolation. For
example, a plugin could have a line in their config file like
files as "root" and "module_root" and are available for interpolation. For
example, a plugin could have a line in their config file like::
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's two semi-colons here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe those are full colons. A lot of programming languages use full colons when only a semicolon would do :P

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oops, this is what I get for reviewing PRs before I'm fully awake. :P Yes, there's two colons here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Wow! We've got some eagle-eyed reviewers here! I'm impressed. However, I did intentionally use two colons. In reStructuredText (which is the de facto standard for docstrings) two colons indicates the start of a literal block (as you'd use for code or other mono-space reformatted blocks).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ahh, gotcha. Good to know, I'll start doing that myself in the future.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Neat! Confusing syntax on their part, but neat!

Comment thread sideboard/config.py
from sideboard.lib import config
plugin_name = os.path.basename(module_dir)
root_dir = os.path.join(config['plugins_dir'], plugin_name.replace('_', '-'))
root_dir = os.path.join(config['plugins_dir'], plugin_name)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What was the reason for getting rid of the replace here? That seems like something that could break some plugins if they were expecting underscores to be replaced with hyphens.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Indeed, we need to keep this replace. Suppose we have a Sideboard plugin called foo-bar. We wanted to follow the standard .deb and .rpm packaging conventions so that when building e.g. an RPM then we would have the package be named foo-bar.rpm rather than foo_bar.rpm, and thus it also looks in /etc/sideboard/plugins.d/foo-bar.cfg for its config file (which is what this line is doing).

Obviously Python modules need to use underscores, which means that a lot of our packaging would do things like drop our main module file into /opt/sideboard/plugins/foo-bar/foo_bar/__init__.py which seems a little silly but is how we've always done it.

MAGFest doesn't care about any of this, because we run everything out of cloned Git repos so we're not doing any actual packaging and don't look in /etc/sideboard for any config files whatsoever. But it's important for projects which do care about packaging, e.g. my day job :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wondered about this. We haven't settled on a standard here: underscores or hyphens in package names. It seems like you've settled on hyphens in your day job, which will probably end up driving this decision, but in our deployments we are still using underscores (for attendee_tournaments and uber_analytics). So we have something like this:

/opt/sideboard/plugins/attendee_tournaments/attendee_tournaments/__init__.py
/opt/sideboard/plugins/uber_analytics/uber_analytics/__init__.py

I opted to remove the call to .replace() as it fits MAGFests current deployment scheme, and I've seen many python packages that use underscores in the package name (logging_unterpolation for example).

As a compromise, how about something like this?

root_dir = os.path.join(config['plugins_dir'], plugin_name)
if '_' in plugin_name and not os.path.exists(root_dir):
    root_dir = os.path.join(config['plugins_dir'], plugin_name.replace('_', '-'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That compromise sounds great!

Comment thread sideboard/config.py Outdated
os.path.join(config_root, 'sideboard-core.cfg'),
os.path.join(config_root, 'sideboard-server.cfg'),
]
os.path.join(config_root, 'sideboard-server.cfg')]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nitpicky stylistic note: Since the opening square bracket is separated, I'd expect the ending square bracket to also be on its own line. It's harder to read if it's at the end of this line instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a classic style argument among programmers! FWIW, my preference is the same as @kitsuta but there doesn't seem to be a single agreed upon style convention for this, and in my not-statistically-significant sample of programmers I've worked with, I think Rob's "end on the same line" version is more common.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am agnostic on this. Happy to change it!

else:
if not isinstance(value, uuid.UUID):
return '%.32x' % uuid.UUID(value)
return uuid.UUID(value).hex
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's not super-clear what this change is for (although it certainly looks more idiomatic at first glance).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this was just code cleanup, since the latter seems like a much better approach to accomplish the same thing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This actually fails on python 3.5 and above:

Python 3.6.0 (default, Jan 23 2017, 11:13:53) 
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import uuid
>>> '%.32x' % uuid.uuid4()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: %x format: an integer is required, not UUID

I ran into this error because I added python 3.5 to sideboard's test environments. It was a small enough change I didn't think it warranted a new issue and separate pull request.

assert ['a', 'b', 'c'] == uniquify(['a', 'b', 'c'])
assert ['a', 'b', 'c', 'd', 'e'] == uniquify(['a', 'b', 'a', 'c', 'a', 'd', 'a', 'e'])
assert ['a'] == uniquify(['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would also have a test here for a list that's out of order -- the test will currently fail (see above comment on uniquify).

assert ['a'] == uniquify(['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'])


@pytest.mark.skipif(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't quite understand this test, since you later set or unset the overrides var during tests. Is the skip here just in case that monkeypatch fails, for some reason?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wanted to write a test that ensured test-defaults.ini was being loaded if SIDEBOARD_CONFIG_OVERRIDES was set in the environment (independently of monkey patching it after the tests have already begun).

However, I didn't want the test to fail if someone just ran the tests from the command line without setting the SIDEBOARD_CONFIG_OVERRIDES, so that's why it's skipped.

This was mostly a sanity check for my own benefit. I'll add a docstring explaining why this test exists.

root_path = '/opt/sideboard'
return (module_path, root_path)

def test_get_module_and_root_dirs_plugin(self, plugin_dirs):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You're hard-coding paths in some places and using os.path in others. Will that cause these tests to fail if run in an unexpected environment?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Our use of os.path.join and os.path.sep is somewhat vestigial. Originally when we were coding Sideboard, we though, "We should use os.path.join on general principle in case we ever need to run this on Windows or something crazy like that." Fast forward to now and I think it's safe to say that Sideboard is definitely a Unix-based system and we simply don't ever intend to support it on any other platform. I still tend to use things like os.path.join anyway just out of habit and good general practice, but it's definitely not a problem to assume / as a path separator IMO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think at this point the forward slash '/' is supported as a separator on all platforms (even windows). But it doesn't matter because these hardcoded paths probably won't exist anyway. The functions being tested aren't actually examining the filesystem, they're just doing string manipulation.

The one exception to that is where you see os.getcwd() being used.

Comment thread tox.ini

[testenv]
setenv="SIDEBOARD_CONFIG_OVERRIDES=test-defaults.ini"
deps= -rrequirements.txt
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are the two 'r's here a typo?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't believe so. If memory serves, this basically says that we should pass the argument -r requirements.txt to pip.

Comment thread sideboard/config.py Outdated

def uniquify(x):
"""
Returns an ordered copy of `x` with duplicate items removed.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would specify what "ordered" means in this context. From the implementation I can see that what you mean is "in the order in which the items originally appeared" but a casual reader might assume you meant "in sorted order".

Comment thread sideboard/config.py Outdated
expect in production


If `is_plugin` is `False` the first of the returned files is:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Confusing wording; this should probably read "the first two returned files are" since we're listing two files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I should note, I changed the plugin parameter to is_plugin for clarity. Generally I'm fine with not using the is_* prefix to indicate a boolean, but for these methods in particular I was initially confused because I kept thinking that plugin was referring to a module, and I had keep going back to read the docstring.

@EliAndrewC
Copy link
Copy Markdown
Contributor

Other than the one thing about the hyphen and a couple of style suggestions, this looks good to me!

Copy link
Copy Markdown
Member

@kitsuta kitsuta left a comment

Choose a reason for hiding this comment

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

+1

Comment thread sideboard/config.py Outdated

"""
seen = set()
return [i for i in x if i not in seen and not seen.add(i)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm actually a fan of list comprehension, so I don't think you need to explode it. It's pretty clear what this function does -- I just wanted to know exactly how it was working. It would be different if this was part of a more complex function, but because it's in its own function it is totally fine as-is.

Comment thread sideboard/config.py
files as "root" and "module_root" and are available for interpolation. For
example, a plugin could have a line in their config file like
files as "root" and "module_root" and are available for interpolation. For
example, a plugin could have a line in their config file like::
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Neat! Confusing syntax on their part, but neat!

Comment thread sideboard/config.py Outdated
]
os.path.join(config_root, 'sideboard-server.cfg')]

override_configs = [os.path.join(root_dir, config_path) for config_path in get_config_overrides()]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, config_bases doesn't read very well, but I think it's fine to say base_configs + config_overrides. It's symmetrical, which is almost as good as being parallel!

@coveralls
Copy link
Copy Markdown

coveralls commented Mar 15, 2017

Coverage Status

Coverage increased (+0.3%) to 77.688% when pulling 0df7cbb on fix-66-sideboard-config-overrides into aa40f7e on master.

Comment thread .travis.yml
- python: "3.4"
env: TOX_ENV=py34
- python: "3.5"
env: TOX_ENV=py35
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This looks more complicated than necessary, but it's the one of the ways to guarantee Travis will find the correct interpreter. You'll find a lot of issues about this if you google "Travis InterpreterNotFound"; see this issue among many others.

Comment thread .coveragerc
tests/*

include =
sideboard/*
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Perhaps a controversial decision; I moved .coveragerc into setup.cfg just to cut down on the number of files in the project directory. I'm happy to put it back if anyone objects!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe that including this kind of thing in setup.cfg is considered more canonically correct these days, so this change seems good.

Comment thread sideboard/config.py

def get_dirnames(pyname, plugin):
def get_module_and_root_dirs(requesting_file_path, is_plugin):
"""
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Function name changed just for clarity. Feel free to suggest a better name, or to object to renaming this function in general!

@RobRuana RobRuana merged commit ed02745 into master Mar 15, 2017
@RobRuana RobRuana deleted the fix-66-sideboard-config-overrides branch March 15, 2017 19:38
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.

4 participants