Coder Social home page Coder Social logo

opster's Introduction

opster's People

Contributors

astanin avatar daevaorn avatar knsd avatar mrzv avatar noxiouz avatar oscarbenjamin avatar piranha avatar qpleple avatar vlasovskikh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

opster's Issues

cram test failed: missing varargs.py

I assume a file called tests/varargs.py has not been added to git.

$ make test
python opster.py
cram tests/*.t
!
--- /stuff/oscar/work/current/software/opster/tests/opster.t
+++ /stuff/oscar/work/current/software/opster/tests/opster.t.err
@@ -262,6 +262,8 @@
There is no problems with handling variable argumentrs and underscores::

$ run varargs.py --test-option test1 var1 var2
-  {'args': ('var1', 'var2'), 'test_option': 'test1'}
+  python: can't open file '/stuff/oscar/work/current/software/opster/tests/varargs.py': [Errno 2] No such file or directory
+  [2]
$ run varargs.py var1 var2
-  {'args': ('var1', 'var2'), 'test_option': 'test'}
+  python: can't open file '/stuff/oscar/work/current/software/opster/tests/varargs.py': [Errno 2] No such file or directory
+  [2]  

# Ran 1 tests, 0 skipped, 1 failed.
make: *** [test] Error 1

Non-global command decorator

Opster's command decorator is really handy for creating a short script like so

from opster import command, dispatch

@command()
def f(a, b=None):
    pass

@command()
def g(c, d=None):
    pass

if __name__ == "__main__":
    dispatch()

The one thing I don't like about this however is that it uses global state.
This means that it's not possible to import from a module that uses the
@command decorator without its commands being included into the global
dispatcher, so it's not possible for two opster scripts to import code from one
another. The alternative syntax doesn't have this problem, but isn't as nice:

from opster import command, dispatch

def f(a, b=None):
    pass

def g(a, b=None):
    pass

cmdtable = {
    'f': (f, [], None),
    'g': (g, [], None),
}

if __name__ == "__main__":
    dispatch()

I wondered about maybe having a non-global version of the decorator:

from opster import command, Dispatcher

dispatcher = Dispatcher()    

@dispatcher.command()
def f(a, b=None):
    pass

@dispatcher.command()
def g(c, d=None):
    pass

if __name__ == "__main__":
    dispatcher.dispatch()

Then opster.py could define the global dispatcher like so:

_dispatcher = Dispatcher()
command = _dispatcher.command
dispatch = _dispatcher.dispatch

What do you think?

I don't think that would be hard to implement. What would be even better but
trickier to implement is if there was if you could add an already decorated
command function:

from myotherscript import somecommand

dispatcher.add_command(somecommand)

or even:

from myotherscript import somedispatcher

dispatcher.add_from(somedispatcher)

These would make it possible for a module to export opster commands for use elsewhere.

New documentation

Since you suggested doing some work to the docs in GH-42, I thought I'd mention that I was thinking it would be good to reorganise the docs somewhat. I was also planning to add some documentation for some of the new features that opster has. I thought about restructuring the docs so that it has pages giving more detail about particular topics, e.g.:

  1. index
  2. overview
  3. positional arguments
  4. option arguments
  5. subcommands
  6. completion
  7. unicode and encodings
  8. python 3 and opster's future plans (backward compatibility, keyword arguments, annotations etc.)

I also thought that maybe the docs/*.rst files could be made into tests that would actually be run by cram (rather than including tests.t in the docs).

Another thought was that the tests in the tests directory could be reorganised to reflect the structure above. The tests directory is a bit of a mess right now - largely my fault :)

py3k

I've been porting some of my scripts over to python 3. One big problem in doing this is that opster does not seem to be ported yet. I have attached a patch that should enable opster to install on python 2.x and 3.x using distutils and the 2to3 tool.

The only problem that is not fixable by the 2to3 tool as far as I can tell is the write function. This function explicitly calls encode on unicode strings. I think that, in python 3.x, this is unnecessary since encoding and decoding of unicode strings is automatic when writing to a "text file". I've just added a check for the python version to ensure that this call to encode is only carried out in python 2.x.

The other issue is that python3 cannot import the 2.x version of opster when running setup.py to build the 3.x version. To solve this, I've replaced the import opster line with a function that reads the contents of opster.py into a string and uses a regex to find __version__ etc.

Otherwise everything works fine, except that I used distutils rather than setuptools. Is there a reason for using setuptools rather than distutils? On my system it prevented me from immediately being able to install the latest version of opster (rather than ubuntu's one), since setuptools was not installed. This dependency on setuptools will be more important if the module actually needs to be built with 2to3 rather than simply copied.

diff --git a/opster.py b/opster.py
index 026d509..eb7ce46 100644
--- a/opster.py
+++ b/opster.py
@@ -24,8 +24,8 @@ except locale.Error:
 def write(text, out=None):
     '''Write output to a given stream (stdout by default)'''
     out = out or sys.stdout
-    if isinstance(text, unicode):
-        return out.write(text.encode(ENCODING))
+    if sys.version_info < (3, 0) and isinstance(text, unicode):
+        text = text.encode(ENCODING)
     out.write(text)

 def err(text):
diff --git a/setup.py b/setup.py
index c06af0f..b9d9153 100755
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,15 @@
 #!/usr/bin/env python

-import os
-from setuptools import setup
-import opster
+import os, re
+
+from distutils.core import setup
+
+# Use 2to3 build conversion if required
+try:
+    from distutils.command.build_py import build_py_2to3 as build_py
+except ImportError:
+    # 2.x
+    from distutils.command.build_py import build_py

 def read(fname):
     return open(os.path.join(os.path.dirname(__file__), fname)).read()
@@ -15,14 +22,21 @@ def desc():
         # no docs
         return info

+# grep opster.py since python 3.x cannot import it before using 2to3
+opster_text = read('opster.py')
+def grep_opsterpy(attrname):
+    pattern = r"{0}\W*=\W*'([^']+)'".format(attrname)
+    strval, = re.findall(pattern, opster_text)
+    return strval
+
 setup(
     name = 'opster',
     description = 'command line parsing speedster',
     long_description = desc(),
     license = 'BSD',
-    version = opster.__version__,
-    author = opster.__author__,
-    author_email = opster.__email__,
+    version = grep_opsterpy('__version__'),
+    author = grep_opsterpy('__author__'),
+    author_email = grep_opsterpy('__email__'),
     url = 'http://github.com/piranha/opster/',
     classifiers = [
         'Environment :: Console',
@@ -34,4 +48,5 @@ setup(
         ],
     py_modules = ['opster'],
     platforms='any',
+    cmdclass={'build_py':build_py}
     )

opster bombs with invalid input for float/int option

Hi again.

I've found that opster generates nice helpful message for the user when an option argument is missing or is not a valid a=b string for dict options, but bombs with a stcktrace if it fails to parse a float/int option. Example:

myscript.py:

import sys
import opster

@opster.command()
def main(number=('n', 1, 'integer option'),
         ratio=('r', 1.0, 'float option')):
    print number, ratio

if __name__ == "__main__":
    main(argv=sys.argv[1:])

If you run python mtscript.py -n you get the message


myscript.py [OPTIONS] 

(no help text available)

options:

 -n --number  integer option (default: 1)
 -r --ratio   float option (default: 1.0)
 -h --help    show help```

However, if you run `python myscript.py -n 1.1` or `python myscript.py -r bad` then you will get something like:
`Traceback (most recent call last):
  File "myscript.py", line 10, in <module>
    main(argv=sys.argv[1:])
  File "/usr/lib/pymodules/python2.7/opster.py", line 110, in inner
    opts, args = catcher(lambda: parse(argv, options_), func.help)
  File "/usr/lib/pymodules/python2.7/opster.py", line 504, in catcher
    return target()
  File "/usr/lib/pymodules/python2.7/opster.py", line 110, in <lambda>
    opts, args = catcher(lambda: parse(argv, options_), func.help)
  File "/usr/lib/pymodules/python2.7/opster.py", line 371, in parse
    state[name] = t(val)
ValueError: invalid literal for int() with base 10: '1.1'`

This can be fixed with the following patch (relative to current git master):

```diff --git a/opster.py b/opster.py
index c9ec3d8..101ec36 100644
--- a/opster.py
+++ b/opster.py
@@ -439,8 +439,13 @@ def process(args, options, preparse=False):
             state[name][k] = v
         elif t in (types.NoneType, types.BooleanType):
             state[name] = not defmap[name]
-        else:
-            state[name] = t(val)
-        elif t in (int, float):
-            try:
-                state[name] = t(val)
-            except ValueError:
-                raise getopt.GetoptError(
-                    "invalid option value '%s' for option '%s'"
- ```
                 % (val, name))

for name in funlist:
state[name] = defmapname```

Changing stdout/stderr encoding

While GH-42 was waiting to get merged (thanks for that) there's been discussion on python-ideas about changing the encoding of stdout/stderr in python:

http://mail.python.org/pipermail/python-ideas/2012-June/015329.html
http://mail.python.org/pipermail/python-ideas/2012-June/015435.html

The general conclusion seems to be that it's a bad idea unless it's done by replacing e.g. sys.stdout in-place once at the beginning of a script/program (before any output is written). The second thread linked above is about finding the best way to do this, so I'll wait to see what they come up with.

In any case, though, I think that opster should probably just leave the output encoding alone. Since the only good way to change the encoding is once at the beginning of a program it should not be up to a library such as opster to do this, rather the application using opster should do this. opster scripts that want to write non-ascii output in a non-default encoding should be advised in the docs to replace sys.stdout and sys.stderr if they need to, with something like the following (or whatever the python-ideas thread comes up with):

import sys, codecs

encoding = 'utf-8'
writer = codecs.getwriter(encoding)

sys.stdout = writer(sys.stdout.buffer)
sys.stderr = writer(sys.stderr.buffer)

Then opster can just output text without doing anything to the encodings, using print which already has automatic encoding detection in Python 2.x (unlike sys.stdout.write). Python works out based on the environment it is running in what the appropriate encoding for stdout and stderr should be. Lets call this encoding ENV_ENCODING. If the script author replaces sys.stdin as above, then they will use ALT_ENCODING. There are six cases to consider:

  1. If stdio was not replaced. opster should assume that the script is happy with the default output encoding and allow print to determine what to do.
    1. Printing str/bytes in Python 2.x: print will simply write the bytes. Since the user didn't specify what encoding they wanted, opster is assuming that the encoding already used for the bytes is acceptable output for the script. This how Python 2.x generally works.
    2. Printing unicode in Python 2.x: print will encode as ENV_ENCODING.
    3. Printing str/unicode in Python 3.x: print will encode as ENV_ENCODING
  2. If stdio was replaced. opster should assume that the script has selected the appropriate encoding and should not interfere with it
    1. Printing str/bytes in Python 2.x: print will pass the bytes through to sys.stdout.write which will coerce them to unicode by decoding as ascii and then reencode them as ALT_ENCODING.
    2. Printing unicode in Python 2.x: print will call sys.stdout.write which will encode as ALT_ENCODING
    3. Printing str/unicode in Python 3.x: print will call sys.stdout.write which will encode as ALT_ENCODING.

The only case that causes problems is 2.1 (str/bytes in Python 2.x with stdio replaced). In Python 2.x if the help or option name is encoded bytes containing non-ascii characters then attempting to write these to the codecs.writer object will lead to a UnicodeDecodeError. The script writer could fix this by declaring them as unicode e.g. u'asd\xe4' but this would cause problems in Python 3.x (because of the annoying unicode literals rule). I think that opster should catch the UnicodeDecodeError and only in that particular case decode as 'utf-8'. It is reasonable to mandate that help strings and option names should be 'utf-8' since this is the official default encoding for Python code.

So the write function in opster.py looks like this:

def write(text, out):
    try:
        print >> out, text
    except UnicodeDecodeError:
        print >> out, text.decode('utf-8')

I think that will correctly handle all cases and allow scripts to replace sys.stdout and sys.stderrin a way that can easily be explained in the docs. Note that UnicodeEncodeErrors are not caught in the above function. It is still an error to try to write unicode that cannot be encoded in the output encoding.

Opster syntax in python 3

Following the merge of keyword-only argument introspection support in GH-31, I thought I'd open a new issue to discuss how opster's syntax in python3 should look. Python3 adds two new features relevant to the syntax used in opster: keyword only arguments and function annotations.

Keyword-only arguments

That opster should allow keyword arguments to be used for specifying options seems obvious to me. I also think that, when possible, opster should drop all support for any other type argument-option translation. Currently the opster syntax (when using introspection) looks like:

@opster.command()
def main(required_arg, optional_arg=None,
         option1=('o', False, 'help for --option1'),
         option2=('O', 'default', 'enter a value for option2'),
         *varargs, **globalopts)
    pass

Using keyword arguments allows it to be rewritten with varargs immediately following the other positional arguments

@opster.command()
def main(required_arg, optional_arg=None, *varargs,
         option1=('o', False, 'help for --option1'),
         option2=('O', 'default', 'enter a value for option2'),
         **globalopts)
    pass

The second form keeps the positional arguments together and shows them as they are used for the script. It means that opster can identify option arguments unambiguously in a way that can be easily explained in the docs by simply saying: Opster infers the positional arguments of the scripts from the positional arguments of main and the options of the script from the keyword-only arguments of main.

Using keyword-only arguments means that main(*args, **options) does exactly what you would hope without needing to wrap the main function as long as the options contains values for option1 and option2. It can be made to match perfectly with the expected behaviour by modifying the default arguments in place with e.g.:

def command():
    def wrapper(func):
        # Add command attribute
        func.command = make_command(func)
        # Replace defaults for kwonly arguments
        for long, (short, default, help) in func.__kwdefaults__.items():
            func.__kwdefaults__[long] = default
        # Return func with command attribute and modified defaults
        return func
    return wrapper

Only using keyword arguments is more robust, easier to document, produces clearer code, and allows opster's internal workings to be simpler. Since opster still supports python 2.6+ and most people still use python 2.x, I don't think it's possible to drop support for non-keyword-only arguments now, but I think it should be a target for the future. Specifically I think that when opster drops support for python 2.x, it should also drop support for the old argument syntax.

Another possibility is to require the keyword-argument only syntax in python 3.x now and allow the old syntax in python 2.x. This will avoid having backward compatibility problems later but at the expense of causing problems now. Lots of other people support both python 2.x and 3.x using 2to3 (like opster does) so anyone using opster like that will need to have a syntax that can work with opster under both python versions. Because of this I think it would be best to allow the old syntax under python 3.x for a while. It would, however, be good to have the new opster 3.x syntax prominently displayed in the docs, so that anyone new to opster is aware of it (and at least knows that the old syntax will not always be available).

Function annotations

The other python 3.x new feature that opster could choose to make use of is function annotations. The syntax of function annotations is

def f(a: b = c):
    pass

This results in f having an attribute f.__annotations__ with the value {'a': b}. Opster can use this to add the additional metadata to an argument representing an option without needing to change the default argument. This means that the default value of the option can be used as the default value for the argument and looks like:

@opster.command()
def main(required_arg, optional_arg=None, *varargs
         option1:('o', 'help for --option1') = False,
         option2:('O', 'enter a value for option2') = 'default',
         **globalopts)
    pass

The advantages of this are that opster can use an officially supported mechanism (annotations). It should be clear that the tuples of option data are there to provide information for opster and what the actual default value for option1 is. Also, the function main now really does work exactly as you would expect with main(*args, **options) in all situations without needing its defaults to be modified.

However, this syntax looks a little strange to me. It seems strange that the default value is so far away from the option name. I think it does make sense for the help string to be last since it could be quite long:

@opster.command()
def main(required_arg, optional_arg=None, *varargs
         option1:('o', 'help for --option1') = False,
         option2:('I', '--option2 has a really long help string that spans'
                ' several lines with lots of useless information') = False,
         option3:('O', 'enter a value for option3') = 'default',
         **globalopts)
    pass

I don't know whether I dislike the appearance of this because annotations are unfamiliar and I'm just used to seeing the old opster syntax. I guess opster's current syntax is a bit strange when you first see it (nothing else in python works the way that opster does). I do think, though, that it is a bad thing to have the important information, (long, short, default) separated by the long help string.

I thought that there could be a backward compatibility problem if opster released a version now that allows keyword-only arguments in python3 but without using annotations and opster later decided to use annotations. However, thinking about it, there is no backward compatibility problem supporting two different mechanisms for introspection is easy if the difference between them is well defined (just check for __annotations__) and if both syntaxes are simple and well defined (it is only the current syntax that is difficult to support because because it needs to workaround the lack of keyword-only arguments).

What do you think?

main() got multiple values for keyword argument

With current master and a script like

# foo.py

from __future__ import print_function

import opster

@opster.command()
def main(arg1,
         opt=('o', False, 'use option'),
         *args):
    print(arg1)
    print(args)

if __name__ == "__main__":
    main.command()

I get the following if I try to call main directly:

>>> import foo
>>> foo.main('a', 'b', 'c')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "opster.py", line 202, in inner
    return call_cmd_regular(func, options_)(*args, **opts)
  File "opster.py", line 890, in inner
    return func(*args, **funckwargs)
TypeError: main() got multiple values for keyword argument 'opt'

Is that a bug? I thought Opster would take care of this but when I actually added it to the doctests in the docs branch it didn't work.

IndexError: list index out of range (run from Intellij Idea)

When running from Intellij Idea under windows, the command line contains the full name of the script with a slash in the style of unix:

C:\pf\python\python.exe D:/projects/tests/opster1.py

In function sysname() is expected to slash os.sep.

Incorrect version in PyPI

Hi,

We tried to use new cool feature "nested commands" from version 3.8 and found that 3.8 in PyPI came from stable branch. The only difference from 3.7 to 3.8 in stable branch is version increment and small change in sysname() function (commit 8f875ef). There is no new functionality and no fixes at all!

Please increment version, merge master to stable and upload it to PyPI again.

Use OpsterError to exit without traceback

Hi again,

opster already supports (automatically) exiting a script if the arguments are invalid for their expected type. There is, however, currently no supported mechanism for rejecting an arguments value if it is valid for the type but not for the script. For example, if an option is of integer type, opster will refuse non-integer arguments, but often only a small number of integers is actually valid for a particular option and opster will accept any integer. The problem is that the normal python response of raising an exception produces a traceback message suitable to programmers but not users.

A useful, but currently undocumented, way of dealing with this situation is to raise OpsterError. In my own scripts I have been (ab)using the opster interface by raising OpsterError so that I can quit out of a (perhaps nested) function call and return an error message to the user. I propose that this could be a documented part of the opster API. If so it would probably be appropriate to define a new error or errors e.g. OpsterValueError/OpsterRuntimeError.

I'd be happy to submit a pull request if it was considered a good idea.

An example of a script that uses this functionality would be:

#!/usr/bin/env python

import sys

from opster import command, OpsterError


def check_num_cpus():
    return 4

def do_stuff_with_cpus(cpus):
    pass


@command()
def main(cpus=('n', 1, 'Number of cpus')):

    # Check we have enough cpus
    Ncpus = check_num_cpus()
    if not cpus in range(1, Ncpus):
        msg = "Number of cpus should be from 1 to {0}"
        raise OpsterError(msg.format(Ncpus))

    # Do stuff
    do_stuff_with_cpus(cpus)


if __name__ == "__main__":
    main.command(sys.argv[1:])

wrong name in error for invalid arguments

Hi again. Thanks for including my patch.

I want to report another issue. This is easiest to explain with an example. Consider the script test.py:

import sys, opster

@opster.command()
def main():
    pass

main.command(sys.argv[1:])

Now run python test.py -h to get:

test.py

(no help text available)

options:

 -h --help  display help

Now run with a positional argument python test.py arg to get:

main: invalid arguments

test.py

(no help text available)

options:

 -h --help  display help

So the help names the script as test.py but the error message shows the function name main. The script user should not see the function name in this situation. This happens because Dispatcher.command is calling name_from_python(func.__name__) which is appropriate for Dispatcher.dispatch but not for Dispatcher.command. The following patch fixes this:

diff --git a/opster.py b/opster.py
index 026d509..479f1e6 100644
--- a/opster.py
+++ b/opster.py
@@ -118,7 +118,7 @@ class Dispatcher(object):
             except TypeError:
                 options_ = []

-            name_ = name or name_from_python(func.__name__)
+            name_ = name or sysname()
             if usage is None:
                 usage_ = guess_usage(func, options_)
             else:

Patch can also be found at:
http://pastebin.com/rLQMNVKK

Option type with specified allowable values

One feature that I find myself most often wanting in opster is to declare an option that accepts only a small number of specific string values

#!/usr/bin/env python

import opster

def slow(*args):
    print 'running slowly', args

def fast(*args):
    print 'running fast', args

def lightning(*args):
    print 'running at lightning speed!', args

algorithms = {'slow': slow, 'fast':fast, 'lightning':lightning}

@opster.command()
def main(algorithm=('a', 'slow', 'algorithm: slow, fast or lightning'),
        *args):
    algo = algorithms[algorithm]
    return algo(*args)

main.command()

Currently, you can use a function to parse the argument:

def parse_algo(optarg):
    if optarg is None:
        optarg = 'slow'
    if optarg not in algorithms:
        raise opster.command.Error('Unrecognised algorithm')
    return algorithms[optarg]

@opster.command()
def main(algorithm=('a', parse_algo, 'algorithm: slow, fast or lightning'),
        *args):
    return algorithm(*args)

the disadvantages of this are that it is verbose and that opster cannot display a default value for --algorithm. Actually it shows

$ ./prog.py -h
prog.py [OPTIONS] [ARGS ...]

(no help text available)

options:

 -a --algorithm  algorithm: slow, fast or lightning (default: <function slow
                 at 0x7f21b54547d0>)
 -h --help       display help

which is a bug since the function should not be printed!

In any case, it is common for an option that accepts a string value to only accept a limited range of string values and it would be good for opster to support this. One way of supporting this would be to add a new option type that allows specifying a range of acceptable values. The basic types that opster does not yet use that could be used for this purpose are tuple and set. assuming that tuple is used, the above could look like

algorithms = {'slow': slow, 'fast':fast, 'lightning':lightning}

@opster.command()
def main(algorithm=('a', ('slow', 'fast', 'lightning'), 'algorithm: slow, fast or lightning'),
        *args):
    return algorithms[algorithm](*args)

And then the first element of the tuple is the default. Opster could display the possible values of the option in the help string or could list the valid values when printing an error message (after rejecting an invalid value).

It occurs to me that the same could be used as a convenient way of restricting the range of other non-string option types. For example

@opster.command()
def main(ncpus=('n', tuple(range(1, NCPUS+1)), 'number of cpus to use')):
    return func(ncpus)

In this case the tuple option type could be defined as follows: The type of a tuple type option is the type of its first element. The option argument will be processed as if it were an option of that type. If the resulting option value is not in the tuple it is rejected as invalid.

The disadvantage of using set instead of tuple is that there is no way to know the default type (since a set is unordered) and a set can only take hashable values, whereas the tuple type could work arbitrary option types (see GH-33).

I'd be happy to write a patch for this, but what do you think about the idea of tuple-type options?

opster, unicode and utf-8

When I first sent patches to opster I didn't really understand unicode or what text encoding such as unicode meant. I've submitted patches that change the way that opster handles unicode output but without really knowing what I was doing, except that I wanted the test suite to pass. The code has been changed in order to pass the test suite under dash, then under Windows and finally under Python 3.

Now I feel that I have a sufficient understanding of unicode, encodings and how python handles them to understand what opster does . And, having thought about it, I've concluded that opster's handling of unicode is incorrect.

UnicodeEncode errors are legitimate errors, particularly for a program that interacts with users on stdout/stderr. My windows terminal is unable to represent Cyrillic script. Python knows that, because the terminal uses code page 1252. The reason that Python's cp1252 codec throws an error is because the terminal cannot display the text that the script is trying to print. The way that opster handles this is to forcibly encode the text as bytes using utf-8 and write them as binary data. The result is that instead of showing the (valid) error message the script silently prints meaningless symbols in my terminal. A programmer who makes the mistake of trying to print Cyrillic script in my terminal should want to know that an error has occurred, rather than have it silently ignored.

Opster is a library rather than an application. If a library wants to be internationalised then it shouldn't override system defaults such as stdio encoding unless instructed to do so by the application using the library. Currently opster has no documented way for an application to tell it what encoding it wants to use for output and it also does not respect the system defaults. In Python 3, files opened as text files have an encoding associated with them including stdout and stderr. Opster should write unicode data to those files letting them use their chosen encoding unless instructed to do otherwise by the application. In Python 2, opster needs to handle the encoding itself, and it should use the default encoding (locale.getpreferredencoding()) unless instructed to do something else by the application.

I propose that the main.command and Dispatcher.dispatch API functions could accept stdout and stderr arguments. These should be file-like objects. Under Python 3, these will automatically be unicode aware and have an associated encoding. Under Python 2 these should be unicode aware if the application wants to write non-ascii output. An application using opster can create the unicode aware file objects using codecs.getwriter or codecs.open with the desired encoding and pass these to opster. I think this is a natural way to support unicode in both Python 2 and Python 3.

It would be useful in this situation to have an encoding argument that can be used as a convenience API for stdout = codecs.getwriter(sys.stdout, encoding) and stderr = codecs.getwriter(sys.stderr, encoding). As a minimum, opster should provide an encoding or output_encoding argument for applications to use (if not the stdout/stderr arguments).

There is a separate issue in relation to GH-38, for input arguments provided on the command line. Python 3 currently decodes these using an encoding (sys.getfilesystemencoding()) that may differ from the encoding used in stdout/stderr. Python 2 does not attempt to decode them. I think that, unless opster is told otherwise by the application, opster should leave these as they are under Python 3 and decode them as sys.getfilesystemencoding() under Python 2. If opster will ever decode these using a non-default encoding then it should accept a separate argument e.g. args_encoding, defaulting to sys.getfilesystemencoding().

This will break some tests under a variety of systems. Each test can be made to work again simply by using the API described above to specify the desired encodings.

I'm happy to write a patch for this, but I wanted to see what you think.

Tests fail to run

[yuri@yv /usr/ports/devel/py-opster/work-py39/opster-5.0/tests]$ pytest .
========================================================================================== test session starts ==========================================================================================
platform freebsd13 -- Python 3.9.16, pytest-7.2.0, pluggy-1.0.0
benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /usr/ports/devel/py-opster/work-py39/opster-5.0
plugins: forked-1.4.0, benchmark-4.0.0, cov-2.9.0, nbval-0.10.0, xdist-2.5.0, hypothesis-6.58.0, flake8-1.1.1, rerunfailures-10.1
collected 0 items                                                                                                                                                                                       

========================================================================================= no tests ran in 0.04s =========================================================================================

Version: 5.0
Python-3.9
Pytest-7.2.0
FreeBSD 13.1

cram test failed: missing varargs.py

I assume a file called tests/varargs.py has not been added to git.

$ make test
python opster.py
cram tests/*.t
!
--- /stuff/oscar/work/current/software/opster/tests/opster.t
+++ /stuff/oscar/work/current/software/opster/tests/opster.t.err
@@ -262,6 +262,8 @@
There is no problems with handling variable argumentrs and underscores::

$ run varargs.py --test-option test1 var1 var2
-  {'args': ('var1', 'var2'), 'test_option': 'test1'}
+  python: can't open file '/stuff/oscar/work/current/software/opster/tests/varargs.py': [Errno 2] No such file or directory
+  [2]
$ run varargs.py var1 var2
-  {'args': ('var1', 'var2'), 'test_option': 'test'}
+  python: can't open file '/stuff/oscar/work/current/software/opster/tests/varargs.py': [Errno 2] No such file or directory
+  [2]  

# Ran 1 tests, 0 skipped, 1 failed.
make: *** [test] Error 1

Python 3 support

What's happening with Python 3 support? The README claims that it's supported, but it's clearly not (there are multiple print statements in the code and other problems). Am I missing some version that's compatible with Python 3?

Incorrect help message when arguments have underscores

Two almost identical functions (only difference is the underscore in the arugment) produce two different help messages

@command()
def show(name, place = '', argument = ('a', False, 'some argument')):
    print(name)

results in

problem.py show [OPTIONS] NAME [PLACE]

[...]

 -a --argument  some argument
@command()
def show(name, place = '', argu_ment = ('a', False, 'some argument')):
    print(name)

results in

problem.py show [OPTIONS] NAME [PLACE] [ARGU_MENT]

[...]

 -a --argu-ment  some argument

The [ARGU_MENT] doesn't belong in the latter command line.

Add a FileOption type

I have one last suggestion for an option type. I think argparse or some other parse supports this. The new arbitrary option type syntax allows opster to open a file for reading, if sys.stdin is given as the default value. Since type(sys.stdin) is file opster will just return file(optarg) which opens a file named optarg for reading. However, it doesn't work for sys.stdout since the file will still be opened for reading and the help message prints the repr of the file instances (both trivial to fix with a special-cased option type).

Do you think it would be good to special case a FileOption type option?

Issues include how to choose the correct encoding, whether the file should be in binary mode mean that it wouldn't be suitable for all cases. Perhaps opster could just supply the common case of wanting to open a file in text mode with utf-8 encoding? Or perhaps the default encoding should be the encoding of the file given as default in the option (e.g. sys.stdin).

Example:

#!/usr/bin/env python

import sys
import re
import opster

@opster.command()
def grep(pattern,
         infile=('i', sys.stdin, 'file to read from'),
         outfile=('o', sys.stdout, 'file to write to')):
    '''
    Write lines from --infile to --outfile only if they match pattern
    '''
    pattern = re.compile(pattern)
    line = ' '
    while line:
        line = infile.readline()
        if pattern.match(line):
            outfile.write(line)
            outfile.flush()

grep.command()

Output:
$ ./script.py
script.py: invalid arguments

script.py [OPTIONS] PATTERN

Write lines from --infile to --outfile only if they match pattern

options:

 -i --infile   file to read from (default: <open file '<stdin>', mode 'r' at
               0x00A64020>)
 -o --outfile  file to write to (default: <open file '<stdout>', mode 'w' at
               0x00A64070>)
 -h --help     display help

$ ./script.py '^#.*$' < setup.py
#!/usr/bin/env python
# Use 2to3 build conversion if required
# grep opster.py since python 3.x cannot import it before using 2to3

$ ./script.py '^#.*$' -i setup.py
#!/usr/bin/env python
# Use 2to3 build conversion if required
# grep opster.py since python 3.x cannot import it before using 2to3

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.