feat(core): 更新py-esptool (version: 4.8.1)
This commit is contained in:
@@ -37,8 +37,8 @@ config = dotenv.find_dotenv(filename=".ampy", usecwd=True)
|
||||
if config:
|
||||
dotenv.load_dotenv(dotenv_path=config)
|
||||
|
||||
import files as files
|
||||
import pyboard as pyboard
|
||||
import ampy.files as files
|
||||
import ampy.pyboard as pyboard
|
||||
|
||||
|
||||
_board = None
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
Click is a simple Python module inspired by the stdlib optparse to make
|
||||
writing command line scripts fun. Unlike other modules, it's based
|
||||
around a simple API that does not come with too much magic and is
|
||||
composable.
|
||||
"""
|
||||
from .core import Argument
|
||||
from .core import BaseCommand
|
||||
from .core import Command
|
||||
from .core import CommandCollection
|
||||
from .core import Context
|
||||
from .core import Group
|
||||
from .core import MultiCommand
|
||||
from .core import Option
|
||||
from .core import Parameter
|
||||
from .decorators import argument
|
||||
from .decorators import command
|
||||
from .decorators import confirmation_option
|
||||
from .decorators import group
|
||||
from .decorators import help_option
|
||||
from .decorators import make_pass_decorator
|
||||
from .decorators import option
|
||||
from .decorators import pass_context
|
||||
from .decorators import pass_obj
|
||||
from .decorators import password_option
|
||||
from .decorators import version_option
|
||||
from .exceptions import Abort
|
||||
from .exceptions import BadArgumentUsage
|
||||
from .exceptions import BadOptionUsage
|
||||
from .exceptions import BadParameter
|
||||
from .exceptions import ClickException
|
||||
from .exceptions import FileError
|
||||
from .exceptions import MissingParameter
|
||||
from .exceptions import NoSuchOption
|
||||
from .exceptions import UsageError
|
||||
from .formatting import HelpFormatter
|
||||
from .formatting import wrap_text
|
||||
from .globals import get_current_context
|
||||
from .parser import OptionParser
|
||||
from .termui import clear
|
||||
from .termui import confirm
|
||||
from .termui import echo_via_pager
|
||||
from .termui import edit
|
||||
from .termui import get_terminal_size
|
||||
from .termui import getchar
|
||||
from .termui import launch
|
||||
from .termui import pause
|
||||
from .termui import progressbar
|
||||
from .termui import prompt
|
||||
from .termui import secho
|
||||
from .termui import style
|
||||
from .termui import unstyle
|
||||
from .types import BOOL
|
||||
from .types import Choice
|
||||
from .types import DateTime
|
||||
from .types import File
|
||||
from .types import FLOAT
|
||||
from .types import FloatRange
|
||||
from .types import INT
|
||||
from .types import IntRange
|
||||
from .types import ParamType
|
||||
from .types import Path
|
||||
from .types import STRING
|
||||
from .types import Tuple
|
||||
from .types import UNPROCESSED
|
||||
from .types import UUID
|
||||
from .utils import echo
|
||||
from .utils import format_filename
|
||||
from .utils import get_app_dir
|
||||
from .utils import get_binary_stream
|
||||
from .utils import get_os_args
|
||||
from .utils import get_text_stream
|
||||
from .utils import open_file
|
||||
|
||||
# Controls if click should emit the warning about the use of unicode
|
||||
# literals.
|
||||
disable_unicode_literals_warning = False
|
||||
|
||||
__version__ = "7.1.2"
|
||||
@@ -1,375 +0,0 @@
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
|
||||
from .core import Argument
|
||||
from .core import MultiCommand
|
||||
from .core import Option
|
||||
from .parser import split_arg_string
|
||||
from .types import Choice
|
||||
from .utils import echo
|
||||
|
||||
try:
|
||||
from collections import abc
|
||||
except ImportError:
|
||||
import collections as abc
|
||||
|
||||
WORDBREAK = "="
|
||||
|
||||
# Note, only BASH version 4.4 and later have the nosort option.
|
||||
COMPLETION_SCRIPT_BASH = """
|
||||
%(complete_func)s() {
|
||||
local IFS=$'\n'
|
||||
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
|
||||
COMP_CWORD=$COMP_CWORD \\
|
||||
%(autocomplete_var)s=complete $1 ) )
|
||||
return 0
|
||||
}
|
||||
|
||||
%(complete_func)setup() {
|
||||
local COMPLETION_OPTIONS=""
|
||||
local BASH_VERSION_ARR=(${BASH_VERSION//./ })
|
||||
# Only BASH version 4.4 and later have the nosort option.
|
||||
if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \
|
||||
&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
|
||||
COMPLETION_OPTIONS="-o nosort"
|
||||
fi
|
||||
|
||||
complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s
|
||||
}
|
||||
|
||||
%(complete_func)setup
|
||||
"""
|
||||
|
||||
COMPLETION_SCRIPT_ZSH = """
|
||||
#compdef %(script_names)s
|
||||
|
||||
%(complete_func)s() {
|
||||
local -a completions
|
||||
local -a completions_with_descriptions
|
||||
local -a response
|
||||
(( ! $+commands[%(script_names)s] )) && return 1
|
||||
|
||||
response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\
|
||||
COMP_CWORD=$((CURRENT-1)) \\
|
||||
%(autocomplete_var)s=\"complete_zsh\" \\
|
||||
%(script_names)s )}")
|
||||
|
||||
for key descr in ${(kv)response}; do
|
||||
if [[ "$descr" == "_" ]]; then
|
||||
completions+=("$key")
|
||||
else
|
||||
completions_with_descriptions+=("$key":"$descr")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$completions_with_descriptions" ]; then
|
||||
_describe -V unsorted completions_with_descriptions -U
|
||||
fi
|
||||
|
||||
if [ -n "$completions" ]; then
|
||||
compadd -U -V unsorted -a completions
|
||||
fi
|
||||
compstate[insert]="automenu"
|
||||
}
|
||||
|
||||
compdef %(complete_func)s %(script_names)s
|
||||
"""
|
||||
|
||||
COMPLETION_SCRIPT_FISH = (
|
||||
"complete --no-files --command %(script_names)s --arguments"
|
||||
' "(env %(autocomplete_var)s=complete_fish'
|
||||
" COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)"
|
||||
' %(script_names)s)"'
|
||||
)
|
||||
|
||||
_completion_scripts = {
|
||||
"bash": COMPLETION_SCRIPT_BASH,
|
||||
"zsh": COMPLETION_SCRIPT_ZSH,
|
||||
"fish": COMPLETION_SCRIPT_FISH,
|
||||
}
|
||||
|
||||
_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")
|
||||
|
||||
|
||||
def get_completion_script(prog_name, complete_var, shell):
|
||||
cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
|
||||
script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH)
|
||||
return (
|
||||
script
|
||||
% {
|
||||
"complete_func": "_{}_completion".format(cf_name),
|
||||
"script_names": prog_name,
|
||||
"autocomplete_var": complete_var,
|
||||
}
|
||||
).strip() + ";"
|
||||
|
||||
|
||||
def resolve_ctx(cli, prog_name, args):
|
||||
"""Parse into a hierarchy of contexts. Contexts are connected
|
||||
through the parent variable.
|
||||
|
||||
:param cli: command definition
|
||||
:param prog_name: the program that is running
|
||||
:param args: full list of args
|
||||
:return: the final context/command parsed
|
||||
"""
|
||||
ctx = cli.make_context(prog_name, args, resilient_parsing=True)
|
||||
args = ctx.protected_args + ctx.args
|
||||
while args:
|
||||
if isinstance(ctx.command, MultiCommand):
|
||||
if not ctx.command.chain:
|
||||
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
|
||||
if cmd is None:
|
||||
return ctx
|
||||
ctx = cmd.make_context(
|
||||
cmd_name, args, parent=ctx, resilient_parsing=True
|
||||
)
|
||||
args = ctx.protected_args + ctx.args
|
||||
else:
|
||||
# Walk chained subcommand contexts saving the last one.
|
||||
while args:
|
||||
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
|
||||
if cmd is None:
|
||||
return ctx
|
||||
sub_ctx = cmd.make_context(
|
||||
cmd_name,
|
||||
args,
|
||||
parent=ctx,
|
||||
allow_extra_args=True,
|
||||
allow_interspersed_args=False,
|
||||
resilient_parsing=True,
|
||||
)
|
||||
args = sub_ctx.args
|
||||
ctx = sub_ctx
|
||||
args = sub_ctx.protected_args + sub_ctx.args
|
||||
else:
|
||||
break
|
||||
return ctx
|
||||
|
||||
|
||||
def start_of_option(param_str):
|
||||
"""
|
||||
:param param_str: param_str to check
|
||||
:return: whether or not this is the start of an option declaration
|
||||
(i.e. starts "-" or "--")
|
||||
"""
|
||||
return param_str and param_str[:1] == "-"
|
||||
|
||||
|
||||
def is_incomplete_option(all_args, cmd_param):
|
||||
"""
|
||||
:param all_args: the full original list of args supplied
|
||||
:param cmd_param: the current command paramter
|
||||
:return: whether or not the last option declaration (i.e. starts
|
||||
"-" or "--") is incomplete and corresponds to this cmd_param. In
|
||||
other words whether this cmd_param option can still accept
|
||||
values
|
||||
"""
|
||||
if not isinstance(cmd_param, Option):
|
||||
return False
|
||||
if cmd_param.is_flag:
|
||||
return False
|
||||
last_option = None
|
||||
for index, arg_str in enumerate(
|
||||
reversed([arg for arg in all_args if arg != WORDBREAK])
|
||||
):
|
||||
if index + 1 > cmd_param.nargs:
|
||||
break
|
||||
if start_of_option(arg_str):
|
||||
last_option = arg_str
|
||||
|
||||
return True if last_option and last_option in cmd_param.opts else False
|
||||
|
||||
|
||||
def is_incomplete_argument(current_params, cmd_param):
|
||||
"""
|
||||
:param current_params: the current params and values for this
|
||||
argument as already entered
|
||||
:param cmd_param: the current command parameter
|
||||
:return: whether or not the last argument is incomplete and
|
||||
corresponds to this cmd_param. In other words whether or not the
|
||||
this cmd_param argument can still accept values
|
||||
"""
|
||||
if not isinstance(cmd_param, Argument):
|
||||
return False
|
||||
current_param_values = current_params[cmd_param.name]
|
||||
if current_param_values is None:
|
||||
return True
|
||||
if cmd_param.nargs == -1:
|
||||
return True
|
||||
if (
|
||||
isinstance(current_param_values, abc.Iterable)
|
||||
and cmd_param.nargs > 1
|
||||
and len(current_param_values) < cmd_param.nargs
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_user_autocompletions(ctx, args, incomplete, cmd_param):
|
||||
"""
|
||||
:param ctx: context associated with the parsed command
|
||||
:param args: full list of args
|
||||
:param incomplete: the incomplete text to autocomplete
|
||||
:param cmd_param: command definition
|
||||
:return: all the possible user-specified completions for the param
|
||||
"""
|
||||
results = []
|
||||
if isinstance(cmd_param.type, Choice):
|
||||
# Choices don't support descriptions.
|
||||
results = [
|
||||
(c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete)
|
||||
]
|
||||
elif cmd_param.autocompletion is not None:
|
||||
dynamic_completions = cmd_param.autocompletion(
|
||||
ctx=ctx, args=args, incomplete=incomplete
|
||||
)
|
||||
results = [
|
||||
c if isinstance(c, tuple) else (c, None) for c in dynamic_completions
|
||||
]
|
||||
return results
|
||||
|
||||
|
||||
def get_visible_commands_starting_with(ctx, starts_with):
|
||||
"""
|
||||
:param ctx: context associated with the parsed command
|
||||
:starts_with: string that visible commands must start with.
|
||||
:return: all visible (not hidden) commands that start with starts_with.
|
||||
"""
|
||||
for c in ctx.command.list_commands(ctx):
|
||||
if c.startswith(starts_with):
|
||||
command = ctx.command.get_command(ctx, c)
|
||||
if not command.hidden:
|
||||
yield command
|
||||
|
||||
|
||||
def add_subcommand_completions(ctx, incomplete, completions_out):
|
||||
# Add subcommand completions.
|
||||
if isinstance(ctx.command, MultiCommand):
|
||||
completions_out.extend(
|
||||
[
|
||||
(c.name, c.get_short_help_str())
|
||||
for c in get_visible_commands_starting_with(ctx, incomplete)
|
||||
]
|
||||
)
|
||||
|
||||
# Walk up the context list and add any other completion
|
||||
# possibilities from chained commands
|
||||
while ctx.parent is not None:
|
||||
ctx = ctx.parent
|
||||
if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
|
||||
remaining_commands = [
|
||||
c
|
||||
for c in get_visible_commands_starting_with(ctx, incomplete)
|
||||
if c.name not in ctx.protected_args
|
||||
]
|
||||
completions_out.extend(
|
||||
[(c.name, c.get_short_help_str()) for c in remaining_commands]
|
||||
)
|
||||
|
||||
|
||||
def get_choices(cli, prog_name, args, incomplete):
|
||||
"""
|
||||
:param cli: command definition
|
||||
:param prog_name: the program that is running
|
||||
:param args: full list of args
|
||||
:param incomplete: the incomplete text to autocomplete
|
||||
:return: all the possible completions for the incomplete
|
||||
"""
|
||||
all_args = copy.deepcopy(args)
|
||||
|
||||
ctx = resolve_ctx(cli, prog_name, args)
|
||||
if ctx is None:
|
||||
return []
|
||||
|
||||
has_double_dash = "--" in all_args
|
||||
|
||||
# In newer versions of bash long opts with '='s are partitioned, but
|
||||
# it's easier to parse without the '='
|
||||
if start_of_option(incomplete) and WORDBREAK in incomplete:
|
||||
partition_incomplete = incomplete.partition(WORDBREAK)
|
||||
all_args.append(partition_incomplete[0])
|
||||
incomplete = partition_incomplete[2]
|
||||
elif incomplete == WORDBREAK:
|
||||
incomplete = ""
|
||||
|
||||
completions = []
|
||||
if not has_double_dash and start_of_option(incomplete):
|
||||
# completions for partial options
|
||||
for param in ctx.command.params:
|
||||
if isinstance(param, Option) and not param.hidden:
|
||||
param_opts = [
|
||||
param_opt
|
||||
for param_opt in param.opts + param.secondary_opts
|
||||
if param_opt not in all_args or param.multiple
|
||||
]
|
||||
completions.extend(
|
||||
[(o, param.help) for o in param_opts if o.startswith(incomplete)]
|
||||
)
|
||||
return completions
|
||||
# completion for option values from user supplied values
|
||||
for param in ctx.command.params:
|
||||
if is_incomplete_option(all_args, param):
|
||||
return get_user_autocompletions(ctx, all_args, incomplete, param)
|
||||
# completion for argument values from user supplied values
|
||||
for param in ctx.command.params:
|
||||
if is_incomplete_argument(ctx.params, param):
|
||||
return get_user_autocompletions(ctx, all_args, incomplete, param)
|
||||
|
||||
add_subcommand_completions(ctx, incomplete, completions)
|
||||
# Sort before returning so that proper ordering can be enforced in custom types.
|
||||
return sorted(completions)
|
||||
|
||||
|
||||
def do_complete(cli, prog_name, include_descriptions):
|
||||
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||
cword = int(os.environ["COMP_CWORD"])
|
||||
args = cwords[1:cword]
|
||||
try:
|
||||
incomplete = cwords[cword]
|
||||
except IndexError:
|
||||
incomplete = ""
|
||||
|
||||
for item in get_choices(cli, prog_name, args, incomplete):
|
||||
echo(item[0])
|
||||
if include_descriptions:
|
||||
# ZSH has trouble dealing with empty array parameters when
|
||||
# returned from commands, use '_' to indicate no description
|
||||
# is present.
|
||||
echo(item[1] if item[1] else "_")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def do_complete_fish(cli, prog_name):
|
||||
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||
incomplete = os.environ["COMP_CWORD"]
|
||||
args = cwords[1:]
|
||||
|
||||
for item in get_choices(cli, prog_name, args, incomplete):
|
||||
if item[1]:
|
||||
echo("{arg}\t{desc}".format(arg=item[0], desc=item[1]))
|
||||
else:
|
||||
echo(item[0])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def bashcomplete(cli, prog_name, complete_var, complete_instr):
|
||||
if "_" in complete_instr:
|
||||
command, shell = complete_instr.split("_", 1)
|
||||
else:
|
||||
command = complete_instr
|
||||
shell = "bash"
|
||||
|
||||
if command == "source":
|
||||
echo(get_completion_script(prog_name, complete_var, shell))
|
||||
return True
|
||||
elif command == "complete":
|
||||
if shell == "fish":
|
||||
return do_complete_fish(cli, prog_name)
|
||||
elif shell in {"bash", "zsh"}:
|
||||
return do_complete(cli, prog_name, shell == "zsh")
|
||||
|
||||
return False
|
||||
@@ -1,786 +0,0 @@
|
||||
# flake8: noqa
|
||||
import codecs
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
CYGWIN = sys.platform.startswith("cygwin")
|
||||
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
|
||||
# Determine local App Engine environment, per Google's own suggestion
|
||||
APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get(
|
||||
"SERVER_SOFTWARE", ""
|
||||
)
|
||||
WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
|
||||
DEFAULT_COLUMNS = 80
|
||||
|
||||
|
||||
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
|
||||
|
||||
|
||||
def get_filesystem_encoding():
|
||||
return sys.getfilesystemencoding() or sys.getdefaultencoding()
|
||||
|
||||
|
||||
def _make_text_stream(
|
||||
stream, encoding, errors, force_readable=False, force_writable=False
|
||||
):
|
||||
if encoding is None:
|
||||
encoding = get_best_encoding(stream)
|
||||
if errors is None:
|
||||
errors = "replace"
|
||||
return _NonClosingTextIOWrapper(
|
||||
stream,
|
||||
encoding,
|
||||
errors,
|
||||
line_buffering=True,
|
||||
force_readable=force_readable,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
|
||||
def is_ascii_encoding(encoding):
|
||||
"""Checks if a given encoding is ascii."""
|
||||
try:
|
||||
return codecs.lookup(encoding).name == "ascii"
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
|
||||
def get_best_encoding(stream):
|
||||
"""Returns the default stream encoding if not found."""
|
||||
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
||||
if is_ascii_encoding(rv):
|
||||
return "utf-8"
|
||||
return rv
|
||||
|
||||
|
||||
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
encoding,
|
||||
errors,
|
||||
force_readable=False,
|
||||
force_writable=False,
|
||||
**extra
|
||||
):
|
||||
self._stream = stream = _FixupStream(stream, force_readable, force_writable)
|
||||
io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)
|
||||
|
||||
# The io module is a place where the Python 3 text behavior
|
||||
# was forced upon Python 2, so we need to unbreak
|
||||
# it to look like Python 2.
|
||||
if PY2:
|
||||
|
||||
def write(self, x):
|
||||
if isinstance(x, str) or is_bytes(x):
|
||||
try:
|
||||
self.flush()
|
||||
except Exception:
|
||||
pass
|
||||
return self.buffer.write(str(x))
|
||||
return io.TextIOWrapper.write(self, x)
|
||||
|
||||
def writelines(self, lines):
|
||||
for line in lines:
|
||||
self.write(line)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.detach()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def isatty(self):
|
||||
# https://bitbucket.org/pypy/pypy/issue/1803
|
||||
return self._stream.isatty()
|
||||
|
||||
|
||||
class _FixupStream(object):
|
||||
"""The new io interface needs more from streams than streams
|
||||
traditionally implement. As such, this fix-up code is necessary in
|
||||
some circumstances.
|
||||
|
||||
The forcing of readable and writable flags are there because some tools
|
||||
put badly patched objects on sys (one such offender are certain version
|
||||
of jupyter notebook).
|
||||
"""
|
||||
|
||||
def __init__(self, stream, force_readable=False, force_writable=False):
|
||||
self._stream = stream
|
||||
self._force_readable = force_readable
|
||||
self._force_writable = force_writable
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._stream, name)
|
||||
|
||||
def read1(self, size):
|
||||
f = getattr(self._stream, "read1", None)
|
||||
if f is not None:
|
||||
return f(size)
|
||||
# We only dispatch to readline instead of read in Python 2 as we
|
||||
# do not want cause problems with the different implementation
|
||||
# of line buffering.
|
||||
if PY2:
|
||||
return self._stream.readline(size)
|
||||
return self._stream.read(size)
|
||||
|
||||
def readable(self):
|
||||
if self._force_readable:
|
||||
return True
|
||||
x = getattr(self._stream, "readable", None)
|
||||
if x is not None:
|
||||
return x()
|
||||
try:
|
||||
self._stream.read(0)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def writable(self):
|
||||
if self._force_writable:
|
||||
return True
|
||||
x = getattr(self._stream, "writable", None)
|
||||
if x is not None:
|
||||
return x()
|
||||
try:
|
||||
self._stream.write("")
|
||||
except Exception:
|
||||
try:
|
||||
self._stream.write(b"")
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def seekable(self):
|
||||
x = getattr(self._stream, "seekable", None)
|
||||
if x is not None:
|
||||
return x()
|
||||
try:
|
||||
self._stream.seek(self._stream.tell())
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if PY2:
|
||||
text_type = unicode
|
||||
raw_input = raw_input
|
||||
string_types = (str, unicode)
|
||||
int_types = (int, long)
|
||||
iteritems = lambda x: x.iteritems()
|
||||
range_type = xrange
|
||||
|
||||
def is_bytes(x):
|
||||
return isinstance(x, (buffer, bytearray))
|
||||
|
||||
_identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||
|
||||
# For Windows, we need to force stdout/stdin/stderr to binary if it's
|
||||
# fetched for that. This obviously is not the most correct way to do
|
||||
# it as it changes global state. Unfortunately, there does not seem to
|
||||
# be a clear better way to do it as just reopening the file in binary
|
||||
# mode does not change anything.
|
||||
#
|
||||
# An option would be to do what Python 3 does and to open the file as
|
||||
# binary only, patch it back to the system, and then use a wrapper
|
||||
# stream that converts newlines. It's not quite clear what's the
|
||||
# correct option here.
|
||||
#
|
||||
# This code also lives in _winconsole for the fallback to the console
|
||||
# emulation stream.
|
||||
#
|
||||
# There are also Windows environments where the `msvcrt` module is not
|
||||
# available (which is why we use try-catch instead of the WIN variable
|
||||
# here), such as the Google App Engine development server on Windows. In
|
||||
# those cases there is just nothing we can do.
|
||||
def set_binary_mode(f):
|
||||
return f
|
||||
|
||||
try:
|
||||
import msvcrt
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
|
||||
def set_binary_mode(f):
|
||||
try:
|
||||
fileno = f.fileno()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
msvcrt.setmode(fileno, os.O_BINARY)
|
||||
return f
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
|
||||
def set_binary_mode(f):
|
||||
try:
|
||||
fileno = f.fileno()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
|
||||
return f
|
||||
|
||||
def isidentifier(x):
|
||||
return _identifier_re.search(x) is not None
|
||||
|
||||
def get_binary_stdin():
|
||||
return set_binary_mode(sys.stdin)
|
||||
|
||||
def get_binary_stdout():
|
||||
_wrap_std_stream("stdout")
|
||||
return set_binary_mode(sys.stdout)
|
||||
|
||||
def get_binary_stderr():
|
||||
_wrap_std_stream("stderr")
|
||||
return set_binary_mode(sys.stderr)
|
||||
|
||||
def get_text_stdin(encoding=None, errors=None):
|
||||
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _make_text_stream(sys.stdin, encoding, errors, force_readable=True)
|
||||
|
||||
def get_text_stdout(encoding=None, errors=None):
|
||||
_wrap_std_stream("stdout")
|
||||
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _make_text_stream(sys.stdout, encoding, errors, force_writable=True)
|
||||
|
||||
def get_text_stderr(encoding=None, errors=None):
|
||||
_wrap_std_stream("stderr")
|
||||
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _make_text_stream(sys.stderr, encoding, errors, force_writable=True)
|
||||
|
||||
def filename_to_ui(value):
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode(get_filesystem_encoding(), "replace")
|
||||
return value
|
||||
|
||||
|
||||
else:
|
||||
import io
|
||||
|
||||
text_type = str
|
||||
raw_input = input
|
||||
string_types = (str,)
|
||||
int_types = (int,)
|
||||
range_type = range
|
||||
isidentifier = lambda x: x.isidentifier()
|
||||
iteritems = lambda x: iter(x.items())
|
||||
|
||||
def is_bytes(x):
|
||||
return isinstance(x, (bytes, memoryview, bytearray))
|
||||
|
||||
def _is_binary_reader(stream, default=False):
|
||||
try:
|
||||
return isinstance(stream.read(0), bytes)
|
||||
except Exception:
|
||||
return default
|
||||
# This happens in some cases where the stream was already
|
||||
# closed. In this case, we assume the default.
|
||||
|
||||
def _is_binary_writer(stream, default=False):
|
||||
try:
|
||||
stream.write(b"")
|
||||
except Exception:
|
||||
try:
|
||||
stream.write("")
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return default
|
||||
return True
|
||||
|
||||
def _find_binary_reader(stream):
|
||||
# We need to figure out if the given stream is already binary.
|
||||
# This can happen because the official docs recommend detaching
|
||||
# the streams to get binary streams. Some code might do this, so
|
||||
# we need to deal with this case explicitly.
|
||||
if _is_binary_reader(stream, False):
|
||||
return stream
|
||||
|
||||
buf = getattr(stream, "buffer", None)
|
||||
|
||||
# Same situation here; this time we assume that the buffer is
|
||||
# actually binary in case it's closed.
|
||||
if buf is not None and _is_binary_reader(buf, True):
|
||||
return buf
|
||||
|
||||
def _find_binary_writer(stream):
|
||||
# We need to figure out if the given stream is already binary.
|
||||
# This can happen because the official docs recommend detatching
|
||||
# the streams to get binary streams. Some code might do this, so
|
||||
# we need to deal with this case explicitly.
|
||||
if _is_binary_writer(stream, False):
|
||||
return stream
|
||||
|
||||
buf = getattr(stream, "buffer", None)
|
||||
|
||||
# Same situation here; this time we assume that the buffer is
|
||||
# actually binary in case it's closed.
|
||||
if buf is not None and _is_binary_writer(buf, True):
|
||||
return buf
|
||||
|
||||
def _stream_is_misconfigured(stream):
|
||||
"""A stream is misconfigured if its encoding is ASCII."""
|
||||
# If the stream does not have an encoding set, we assume it's set
|
||||
# to ASCII. This appears to happen in certain unittest
|
||||
# environments. It's not quite clear what the correct behavior is
|
||||
# but this at least will force Click to recover somehow.
|
||||
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
|
||||
|
||||
def _is_compat_stream_attr(stream, attr, value):
|
||||
"""A stream attribute is compatible if it is equal to the
|
||||
desired value or the desired value is unset and the attribute
|
||||
has a value.
|
||||
"""
|
||||
stream_value = getattr(stream, attr, None)
|
||||
return stream_value == value or (value is None and stream_value is not None)
|
||||
|
||||
def _is_compatible_text_stream(stream, encoding, errors):
|
||||
"""Check if a stream's encoding and errors attributes are
|
||||
compatible with the desired values.
|
||||
"""
|
||||
return _is_compat_stream_attr(
|
||||
stream, "encoding", encoding
|
||||
) and _is_compat_stream_attr(stream, "errors", errors)
|
||||
|
||||
def _force_correct_text_stream(
|
||||
text_stream,
|
||||
encoding,
|
||||
errors,
|
||||
is_binary,
|
||||
find_binary,
|
||||
force_readable=False,
|
||||
force_writable=False,
|
||||
):
|
||||
if is_binary(text_stream, False):
|
||||
binary_reader = text_stream
|
||||
else:
|
||||
# If the stream looks compatible, and won't default to a
|
||||
# misconfigured ascii encoding, return it as-is.
|
||||
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
|
||||
encoding is None and _stream_is_misconfigured(text_stream)
|
||||
):
|
||||
return text_stream
|
||||
|
||||
# Otherwise, get the underlying binary reader.
|
||||
binary_reader = find_binary(text_stream)
|
||||
|
||||
# If that's not possible, silently use the original reader
|
||||
# and get mojibake instead of exceptions.
|
||||
if binary_reader is None:
|
||||
return text_stream
|
||||
|
||||
# Default errors to replace instead of strict in order to get
|
||||
# something that works.
|
||||
if errors is None:
|
||||
errors = "replace"
|
||||
|
||||
# Wrap the binary stream in a text stream with the correct
|
||||
# encoding parameters.
|
||||
return _make_text_stream(
|
||||
binary_reader,
|
||||
encoding,
|
||||
errors,
|
||||
force_readable=force_readable,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False):
|
||||
return _force_correct_text_stream(
|
||||
text_reader,
|
||||
encoding,
|
||||
errors,
|
||||
_is_binary_reader,
|
||||
_find_binary_reader,
|
||||
force_readable=force_readable,
|
||||
)
|
||||
|
||||
def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False):
|
||||
return _force_correct_text_stream(
|
||||
text_writer,
|
||||
encoding,
|
||||
errors,
|
||||
_is_binary_writer,
|
||||
_find_binary_writer,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
def get_binary_stdin():
|
||||
reader = _find_binary_reader(sys.stdin)
|
||||
if reader is None:
|
||||
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
|
||||
return reader
|
||||
|
||||
def get_binary_stdout():
|
||||
writer = _find_binary_writer(sys.stdout)
|
||||
if writer is None:
|
||||
raise RuntimeError(
|
||||
"Was not able to determine binary stream for sys.stdout."
|
||||
)
|
||||
return writer
|
||||
|
||||
def get_binary_stderr():
|
||||
writer = _find_binary_writer(sys.stderr)
|
||||
if writer is None:
|
||||
raise RuntimeError(
|
||||
"Was not able to determine binary stream for sys.stderr."
|
||||
)
|
||||
return writer
|
||||
|
||||
def get_text_stdin(encoding=None, errors=None):
|
||||
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _force_correct_text_reader(
|
||||
sys.stdin, encoding, errors, force_readable=True
|
||||
)
|
||||
|
||||
def get_text_stdout(encoding=None, errors=None):
|
||||
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _force_correct_text_writer(
|
||||
sys.stdout, encoding, errors, force_writable=True
|
||||
)
|
||||
|
||||
def get_text_stderr(encoding=None, errors=None):
|
||||
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _force_correct_text_writer(
|
||||
sys.stderr, encoding, errors, force_writable=True
|
||||
)
|
||||
|
||||
def filename_to_ui(value):
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode(get_filesystem_encoding(), "replace")
|
||||
else:
|
||||
value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace")
|
||||
return value
|
||||
|
||||
|
||||
def get_streerror(e, default=None):
|
||||
if hasattr(e, "strerror"):
|
||||
msg = e.strerror
|
||||
else:
|
||||
if default is not None:
|
||||
msg = default
|
||||
else:
|
||||
msg = str(e)
|
||||
if isinstance(msg, bytes):
|
||||
msg = msg.decode("utf-8", "replace")
|
||||
return msg
|
||||
|
||||
|
||||
def _wrap_io_open(file, mode, encoding, errors):
|
||||
"""On Python 2, :func:`io.open` returns a text file wrapper that
|
||||
requires passing ``unicode`` to ``write``. Need to open the file in
|
||||
binary mode then wrap it in a subclass that can write ``str`` and
|
||||
``unicode``.
|
||||
|
||||
Also handles not passing ``encoding`` and ``errors`` in binary mode.
|
||||
"""
|
||||
binary = "b" in mode
|
||||
|
||||
if binary:
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = {"encoding": encoding, "errors": errors}
|
||||
|
||||
if not PY2 or binary:
|
||||
return io.open(file, mode, **kwargs)
|
||||
|
||||
f = io.open(file, "{}b".format(mode.replace("t", "")))
|
||||
return _make_text_stream(f, **kwargs)
|
||||
|
||||
|
||||
def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False):
|
||||
binary = "b" in mode
|
||||
|
||||
# Standard streams first. These are simple because they don't need
|
||||
# special handling for the atomic flag. It's entirely ignored.
|
||||
if filename == "-":
|
||||
if any(m in mode for m in ["w", "a", "x"]):
|
||||
if binary:
|
||||
return get_binary_stdout(), False
|
||||
return get_text_stdout(encoding=encoding, errors=errors), False
|
||||
if binary:
|
||||
return get_binary_stdin(), False
|
||||
return get_text_stdin(encoding=encoding, errors=errors), False
|
||||
|
||||
# Non-atomic writes directly go out through the regular open functions.
|
||||
if not atomic:
|
||||
return _wrap_io_open(filename, mode, encoding, errors), True
|
||||
|
||||
# Some usability stuff for atomic writes
|
||||
if "a" in mode:
|
||||
raise ValueError(
|
||||
"Appending to an existing file is not supported, because that"
|
||||
" would involve an expensive `copy`-operation to a temporary"
|
||||
" file. Open the file in normal `w`-mode and copy explicitly"
|
||||
" if that's what you're after."
|
||||
)
|
||||
if "x" in mode:
|
||||
raise ValueError("Use the `overwrite`-parameter instead.")
|
||||
if "w" not in mode:
|
||||
raise ValueError("Atomic writes only make sense with `w`-mode.")
|
||||
|
||||
# Atomic writes are more complicated. They work by opening a file
|
||||
# as a proxy in the same folder and then using the fdopen
|
||||
# functionality to wrap it in a Python file. Then we wrap it in an
|
||||
# atomic file that moves the file over on close.
|
||||
import errno
|
||||
import random
|
||||
|
||||
try:
|
||||
perm = os.stat(filename).st_mode
|
||||
except OSError:
|
||||
perm = None
|
||||
|
||||
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
||||
|
||||
if binary:
|
||||
flags |= getattr(os, "O_BINARY", 0)
|
||||
|
||||
while True:
|
||||
tmp_filename = os.path.join(
|
||||
os.path.dirname(filename),
|
||||
".__atomic-write{:08x}".format(random.randrange(1 << 32)),
|
||||
)
|
||||
try:
|
||||
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
|
||||
break
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST or (
|
||||
os.name == "nt"
|
||||
and e.errno == errno.EACCES
|
||||
and os.path.isdir(e.filename)
|
||||
and os.access(e.filename, os.W_OK)
|
||||
):
|
||||
continue
|
||||
raise
|
||||
|
||||
if perm is not None:
|
||||
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
|
||||
|
||||
f = _wrap_io_open(fd, mode, encoding, errors)
|
||||
return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True
|
||||
|
||||
|
||||
# Used in a destructor call, needs extra protection from interpreter cleanup.
|
||||
if hasattr(os, "replace"):
|
||||
_replace = os.replace
|
||||
_can_replace = True
|
||||
else:
|
||||
_replace = os.rename
|
||||
_can_replace = not WIN
|
||||
|
||||
|
||||
class _AtomicFile(object):
|
||||
def __init__(self, f, tmp_filename, real_filename):
|
||||
self._f = f
|
||||
self._tmp_filename = tmp_filename
|
||||
self._real_filename = real_filename
|
||||
self.closed = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._real_filename
|
||||
|
||||
def close(self, delete=False):
|
||||
if self.closed:
|
||||
return
|
||||
self._f.close()
|
||||
if not _can_replace:
|
||||
try:
|
||||
os.remove(self._real_filename)
|
||||
except OSError:
|
||||
pass
|
||||
_replace(self._tmp_filename, self._real_filename)
|
||||
self.closed = True
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._f, name)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
self.close(delete=exc_type is not None)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._f)
|
||||
|
||||
|
||||
auto_wrap_for_ansi = None
|
||||
colorama = None
|
||||
get_winterm_size = None
|
||||
|
||||
|
||||
def strip_ansi(value):
|
||||
return _ansi_re.sub("", value)
|
||||
|
||||
|
||||
def _is_jupyter_kernel_output(stream):
|
||||
if WIN:
|
||||
# TODO: Couldn't test on Windows, should't try to support until
|
||||
# someone tests the details wrt colorama.
|
||||
return
|
||||
|
||||
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
||||
stream = stream._stream
|
||||
|
||||
return stream.__class__.__module__.startswith("ipykernel.")
|
||||
|
||||
|
||||
def should_strip_ansi(stream=None, color=None):
|
||||
if color is None:
|
||||
if stream is None:
|
||||
stream = sys.stdin
|
||||
return not isatty(stream) and not _is_jupyter_kernel_output(stream)
|
||||
return not color
|
||||
|
||||
|
||||
# If we're on Windows, we provide transparent integration through
|
||||
# colorama. This will make ANSI colors through the echo function
|
||||
# work automatically.
|
||||
if WIN:
|
||||
# Windows has a smaller terminal
|
||||
DEFAULT_COLUMNS = 79
|
||||
|
||||
from ._winconsole import _get_windows_console_stream, _wrap_std_stream
|
||||
|
||||
def _get_argv_encoding():
|
||||
import locale
|
||||
|
||||
return locale.getpreferredencoding()
|
||||
|
||||
if PY2:
|
||||
|
||||
def raw_input(prompt=""):
|
||||
sys.stderr.flush()
|
||||
if prompt:
|
||||
stdout = _default_text_stdout()
|
||||
stdout.write(prompt)
|
||||
stdin = _default_text_stdin()
|
||||
return stdin.readline().rstrip("\r\n")
|
||||
|
||||
try:
|
||||
import colorama
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
_ansi_stream_wrappers = WeakKeyDictionary()
|
||||
|
||||
def auto_wrap_for_ansi(stream, color=None):
|
||||
"""This function wraps a stream so that calls through colorama
|
||||
are issued to the win32 console API to recolor on demand. It
|
||||
also ensures to reset the colors if a write call is interrupted
|
||||
to not destroy the console afterwards.
|
||||
"""
|
||||
try:
|
||||
cached = _ansi_stream_wrappers.get(stream)
|
||||
except Exception:
|
||||
cached = None
|
||||
if cached is not None:
|
||||
return cached
|
||||
strip = should_strip_ansi(stream, color)
|
||||
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
||||
rv = ansi_wrapper.stream
|
||||
_write = rv.write
|
||||
|
||||
def _safe_write(s):
|
||||
try:
|
||||
return _write(s)
|
||||
except:
|
||||
ansi_wrapper.reset_all()
|
||||
raise
|
||||
|
||||
rv.write = _safe_write
|
||||
try:
|
||||
_ansi_stream_wrappers[stream] = rv
|
||||
except Exception:
|
||||
pass
|
||||
return rv
|
||||
|
||||
def get_winterm_size():
|
||||
win = colorama.win32.GetConsoleScreenBufferInfo(
|
||||
colorama.win32.STDOUT
|
||||
).srWindow
|
||||
return win.Right - win.Left, win.Bottom - win.Top
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def _get_argv_encoding():
|
||||
return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
|
||||
|
||||
_get_windows_console_stream = lambda *x: None
|
||||
_wrap_std_stream = lambda *x: None
|
||||
|
||||
|
||||
def term_len(x):
|
||||
return len(strip_ansi(x))
|
||||
|
||||
|
||||
def isatty(stream):
|
||||
try:
|
||||
return stream.isatty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _make_cached_stream_func(src_func, wrapper_func):
|
||||
cache = WeakKeyDictionary()
|
||||
|
||||
def func():
|
||||
stream = src_func()
|
||||
try:
|
||||
rv = cache.get(stream)
|
||||
except Exception:
|
||||
rv = None
|
||||
if rv is not None:
|
||||
return rv
|
||||
rv = wrapper_func()
|
||||
try:
|
||||
stream = src_func() # In case wrapper_func() modified the stream
|
||||
cache[stream] = rv
|
||||
except Exception:
|
||||
pass
|
||||
return rv
|
||||
|
||||
return func
|
||||
|
||||
|
||||
_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
|
||||
_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
|
||||
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
|
||||
|
||||
|
||||
binary_streams = {
|
||||
"stdin": get_binary_stdin,
|
||||
"stdout": get_binary_stdout,
|
||||
"stderr": get_binary_stderr,
|
||||
}
|
||||
|
||||
text_streams = {
|
||||
"stdin": get_text_stdin,
|
||||
"stdout": get_text_stdout,
|
||||
"stderr": get_text_stderr,
|
||||
}
|
||||
@@ -1,657 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module contains implementations for the termui module. To keep the
|
||||
import time of Click down, some infrequently used functionality is
|
||||
placed in this module and only imported as needed.
|
||||
"""
|
||||
import contextlib
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ._compat import _default_text_stdout
|
||||
from ._compat import CYGWIN
|
||||
from ._compat import get_best_encoding
|
||||
from ._compat import int_types
|
||||
from ._compat import isatty
|
||||
from ._compat import open_stream
|
||||
from ._compat import range_type
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import term_len
|
||||
from ._compat import WIN
|
||||
from .exceptions import ClickException
|
||||
from .utils import echo
|
||||
|
||||
if os.name == "nt":
|
||||
BEFORE_BAR = "\r"
|
||||
AFTER_BAR = "\n"
|
||||
else:
|
||||
BEFORE_BAR = "\r\033[?25l"
|
||||
AFTER_BAR = "\033[?25h\n"
|
||||
|
||||
|
||||
def _length_hint(obj):
|
||||
"""Returns the length hint of an object."""
|
||||
try:
|
||||
return len(obj)
|
||||
except (AttributeError, TypeError):
|
||||
try:
|
||||
get_hint = type(obj).__length_hint__
|
||||
except AttributeError:
|
||||
return None
|
||||
try:
|
||||
hint = get_hint(obj)
|
||||
except TypeError:
|
||||
return None
|
||||
if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0:
|
||||
return None
|
||||
return hint
|
||||
|
||||
|
||||
class ProgressBar(object):
|
||||
def __init__(
|
||||
self,
|
||||
iterable,
|
||||
length=None,
|
||||
fill_char="#",
|
||||
empty_char=" ",
|
||||
bar_template="%(bar)s",
|
||||
info_sep=" ",
|
||||
show_eta=True,
|
||||
show_percent=None,
|
||||
show_pos=False,
|
||||
item_show_func=None,
|
||||
label=None,
|
||||
file=None,
|
||||
color=None,
|
||||
width=30,
|
||||
):
|
||||
self.fill_char = fill_char
|
||||
self.empty_char = empty_char
|
||||
self.bar_template = bar_template
|
||||
self.info_sep = info_sep
|
||||
self.show_eta = show_eta
|
||||
self.show_percent = show_percent
|
||||
self.show_pos = show_pos
|
||||
self.item_show_func = item_show_func
|
||||
self.label = label or ""
|
||||
if file is None:
|
||||
file = _default_text_stdout()
|
||||
self.file = file
|
||||
self.color = color
|
||||
self.width = width
|
||||
self.autowidth = width == 0
|
||||
|
||||
if length is None:
|
||||
length = _length_hint(iterable)
|
||||
if iterable is None:
|
||||
if length is None:
|
||||
raise TypeError("iterable or length is required")
|
||||
iterable = range_type(length)
|
||||
self.iter = iter(iterable)
|
||||
self.length = length
|
||||
self.length_known = length is not None
|
||||
self.pos = 0
|
||||
self.avg = []
|
||||
self.start = self.last_eta = time.time()
|
||||
self.eta_known = False
|
||||
self.finished = False
|
||||
self.max_width = None
|
||||
self.entered = False
|
||||
self.current_item = None
|
||||
self.is_hidden = not isatty(self.file)
|
||||
self._last_line = None
|
||||
self.short_limit = 0.5
|
||||
|
||||
def __enter__(self):
|
||||
self.entered = True
|
||||
self.render_progress()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
self.render_finish()
|
||||
|
||||
def __iter__(self):
|
||||
if not self.entered:
|
||||
raise RuntimeError("You need to use progress bars in a with block.")
|
||||
self.render_progress()
|
||||
return self.generator()
|
||||
|
||||
def __next__(self):
|
||||
# Iteration is defined in terms of a generator function,
|
||||
# returned by iter(self); use that to define next(). This works
|
||||
# because `self.iter` is an iterable consumed by that generator,
|
||||
# so it is re-entry safe. Calling `next(self.generator())`
|
||||
# twice works and does "what you want".
|
||||
return next(iter(self))
|
||||
|
||||
# Python 2 compat
|
||||
next = __next__
|
||||
|
||||
def is_fast(self):
|
||||
return time.time() - self.start <= self.short_limit
|
||||
|
||||
def render_finish(self):
|
||||
if self.is_hidden or self.is_fast():
|
||||
return
|
||||
self.file.write(AFTER_BAR)
|
||||
self.file.flush()
|
||||
|
||||
@property
|
||||
def pct(self):
|
||||
if self.finished:
|
||||
return 1.0
|
||||
return min(self.pos / (float(self.length) or 1), 1.0)
|
||||
|
||||
@property
|
||||
def time_per_iteration(self):
|
||||
if not self.avg:
|
||||
return 0.0
|
||||
return sum(self.avg) / float(len(self.avg))
|
||||
|
||||
@property
|
||||
def eta(self):
|
||||
if self.length_known and not self.finished:
|
||||
return self.time_per_iteration * (self.length - self.pos)
|
||||
return 0.0
|
||||
|
||||
def format_eta(self):
|
||||
if self.eta_known:
|
||||
t = int(self.eta)
|
||||
seconds = t % 60
|
||||
t //= 60
|
||||
minutes = t % 60
|
||||
t //= 60
|
||||
hours = t % 24
|
||||
t //= 24
|
||||
if t > 0:
|
||||
return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds)
|
||||
else:
|
||||
return "{:02}:{:02}:{:02}".format(hours, minutes, seconds)
|
||||
return ""
|
||||
|
||||
def format_pos(self):
|
||||
pos = str(self.pos)
|
||||
if self.length_known:
|
||||
pos += "/{}".format(self.length)
|
||||
return pos
|
||||
|
||||
def format_pct(self):
|
||||
return "{: 4}%".format(int(self.pct * 100))[1:]
|
||||
|
||||
def format_bar(self):
|
||||
if self.length_known:
|
||||
bar_length = int(self.pct * self.width)
|
||||
bar = self.fill_char * bar_length
|
||||
bar += self.empty_char * (self.width - bar_length)
|
||||
elif self.finished:
|
||||
bar = self.fill_char * self.width
|
||||
else:
|
||||
bar = list(self.empty_char * (self.width or 1))
|
||||
if self.time_per_iteration != 0:
|
||||
bar[
|
||||
int(
|
||||
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
||||
* self.width
|
||||
)
|
||||
] = self.fill_char
|
||||
bar = "".join(bar)
|
||||
return bar
|
||||
|
||||
def format_progress_line(self):
|
||||
show_percent = self.show_percent
|
||||
|
||||
info_bits = []
|
||||
if self.length_known and show_percent is None:
|
||||
show_percent = not self.show_pos
|
||||
|
||||
if self.show_pos:
|
||||
info_bits.append(self.format_pos())
|
||||
if show_percent:
|
||||
info_bits.append(self.format_pct())
|
||||
if self.show_eta and self.eta_known and not self.finished:
|
||||
info_bits.append(self.format_eta())
|
||||
if self.item_show_func is not None:
|
||||
item_info = self.item_show_func(self.current_item)
|
||||
if item_info is not None:
|
||||
info_bits.append(item_info)
|
||||
|
||||
return (
|
||||
self.bar_template
|
||||
% {
|
||||
"label": self.label,
|
||||
"bar": self.format_bar(),
|
||||
"info": self.info_sep.join(info_bits),
|
||||
}
|
||||
).rstrip()
|
||||
|
||||
def render_progress(self):
|
||||
from .termui import get_terminal_size
|
||||
|
||||
if self.is_hidden:
|
||||
return
|
||||
|
||||
buf = []
|
||||
# Update width in case the terminal has been resized
|
||||
if self.autowidth:
|
||||
old_width = self.width
|
||||
self.width = 0
|
||||
clutter_length = term_len(self.format_progress_line())
|
||||
new_width = max(0, get_terminal_size()[0] - clutter_length)
|
||||
if new_width < old_width:
|
||||
buf.append(BEFORE_BAR)
|
||||
buf.append(" " * self.max_width)
|
||||
self.max_width = new_width
|
||||
self.width = new_width
|
||||
|
||||
clear_width = self.width
|
||||
if self.max_width is not None:
|
||||
clear_width = self.max_width
|
||||
|
||||
buf.append(BEFORE_BAR)
|
||||
line = self.format_progress_line()
|
||||
line_len = term_len(line)
|
||||
if self.max_width is None or self.max_width < line_len:
|
||||
self.max_width = line_len
|
||||
|
||||
buf.append(line)
|
||||
buf.append(" " * (clear_width - line_len))
|
||||
line = "".join(buf)
|
||||
# Render the line only if it changed.
|
||||
|
||||
if line != self._last_line and not self.is_fast():
|
||||
self._last_line = line
|
||||
echo(line, file=self.file, color=self.color, nl=False)
|
||||
self.file.flush()
|
||||
|
||||
def make_step(self, n_steps):
|
||||
self.pos += n_steps
|
||||
if self.length_known and self.pos >= self.length:
|
||||
self.finished = True
|
||||
|
||||
if (time.time() - self.last_eta) < 1.0:
|
||||
return
|
||||
|
||||
self.last_eta = time.time()
|
||||
|
||||
# self.avg is a rolling list of length <= 7 of steps where steps are
|
||||
# defined as time elapsed divided by the total progress through
|
||||
# self.length.
|
||||
if self.pos:
|
||||
step = (time.time() - self.start) / self.pos
|
||||
else:
|
||||
step = time.time() - self.start
|
||||
|
||||
self.avg = self.avg[-6:] + [step]
|
||||
|
||||
self.eta_known = self.length_known
|
||||
|
||||
def update(self, n_steps):
|
||||
self.make_step(n_steps)
|
||||
self.render_progress()
|
||||
|
||||
def finish(self):
|
||||
self.eta_known = 0
|
||||
self.current_item = None
|
||||
self.finished = True
|
||||
|
||||
def generator(self):
|
||||
"""Return a generator which yields the items added to the bar
|
||||
during construction, and updates the progress bar *after* the
|
||||
yielded block returns.
|
||||
"""
|
||||
# WARNING: the iterator interface for `ProgressBar` relies on
|
||||
# this and only works because this is a simple generator which
|
||||
# doesn't create or manage additional state. If this function
|
||||
# changes, the impact should be evaluated both against
|
||||
# `iter(bar)` and `next(bar)`. `next()` in particular may call
|
||||
# `self.generator()` repeatedly, and this must remain safe in
|
||||
# order for that interface to work.
|
||||
if not self.entered:
|
||||
raise RuntimeError("You need to use progress bars in a with block.")
|
||||
|
||||
if self.is_hidden:
|
||||
for rv in self.iter:
|
||||
yield rv
|
||||
else:
|
||||
for rv in self.iter:
|
||||
self.current_item = rv
|
||||
yield rv
|
||||
self.update(1)
|
||||
self.finish()
|
||||
self.render_progress()
|
||||
|
||||
|
||||
def pager(generator, color=None):
|
||||
"""Decide what method to use for paging through text."""
|
||||
stdout = _default_text_stdout()
|
||||
if not isatty(sys.stdin) or not isatty(stdout):
|
||||
return _nullpager(stdout, generator, color)
|
||||
pager_cmd = (os.environ.get("PAGER", None) or "").strip()
|
||||
if pager_cmd:
|
||||
if WIN:
|
||||
return _tempfilepager(generator, pager_cmd, color)
|
||||
return _pipepager(generator, pager_cmd, color)
|
||||
if os.environ.get("TERM") in ("dumb", "emacs"):
|
||||
return _nullpager(stdout, generator, color)
|
||||
if WIN or sys.platform.startswith("os2"):
|
||||
return _tempfilepager(generator, "more <", color)
|
||||
if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0:
|
||||
return _pipepager(generator, "less", color)
|
||||
|
||||
import tempfile
|
||||
|
||||
fd, filename = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
try:
|
||||
if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0:
|
||||
return _pipepager(generator, "more", color)
|
||||
return _nullpager(stdout, generator, color)
|
||||
finally:
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
def _pipepager(generator, cmd, color):
|
||||
"""Page through text by feeding it to another program. Invoking a
|
||||
pager through this might support colors.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
env = dict(os.environ)
|
||||
|
||||
# If we're piping to less we might support colors under the
|
||||
# condition that
|
||||
cmd_detail = cmd.rsplit("/", 1)[-1].split()
|
||||
if color is None and cmd_detail[0] == "less":
|
||||
less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:]))
|
||||
if not less_flags:
|
||||
env["LESS"] = "-R"
|
||||
color = True
|
||||
elif "r" in less_flags or "R" in less_flags:
|
||||
color = True
|
||||
|
||||
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
|
||||
encoding = get_best_encoding(c.stdin)
|
||||
try:
|
||||
for text in generator:
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
|
||||
c.stdin.write(text.encode(encoding, "replace"))
|
||||
except (IOError, KeyboardInterrupt):
|
||||
pass
|
||||
else:
|
||||
c.stdin.close()
|
||||
|
||||
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
||||
# search or other commands inside less).
|
||||
#
|
||||
# That means when the user hits ^C, the parent process (click) terminates,
|
||||
# but less is still alive, paging the output and messing up the terminal.
|
||||
#
|
||||
# If the user wants to make the pager exit on ^C, they should set
|
||||
# `LESS='-K'`. It's not our decision to make.
|
||||
while True:
|
||||
try:
|
||||
c.wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
def _tempfilepager(generator, cmd, color):
|
||||
"""Page through text by invoking a program on a temporary file."""
|
||||
import tempfile
|
||||
|
||||
filename = tempfile.mktemp()
|
||||
# TODO: This never terminates if the passed generator never terminates.
|
||||
text = "".join(generator)
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
encoding = get_best_encoding(sys.stdout)
|
||||
with open_stream(filename, "wb")[0] as f:
|
||||
f.write(text.encode(encoding))
|
||||
try:
|
||||
os.system('{} "{}"'.format(cmd, filename))
|
||||
finally:
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
def _nullpager(stream, generator, color):
|
||||
"""Simply print unformatted text. This is the ultimate fallback."""
|
||||
for text in generator:
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
stream.write(text)
|
||||
|
||||
|
||||
class Editor(object):
|
||||
def __init__(self, editor=None, env=None, require_save=True, extension=".txt"):
|
||||
self.editor = editor
|
||||
self.env = env
|
||||
self.require_save = require_save
|
||||
self.extension = extension
|
||||
|
||||
def get_editor(self):
|
||||
if self.editor is not None:
|
||||
return self.editor
|
||||
for key in "VISUAL", "EDITOR":
|
||||
rv = os.environ.get(key)
|
||||
if rv:
|
||||
return rv
|
||||
if WIN:
|
||||
return "notepad"
|
||||
for editor in "sensible-editor", "vim", "nano":
|
||||
if os.system("which {} >/dev/null 2>&1".format(editor)) == 0:
|
||||
return editor
|
||||
return "vi"
|
||||
|
||||
def edit_file(self, filename):
|
||||
import subprocess
|
||||
|
||||
editor = self.get_editor()
|
||||
if self.env:
|
||||
environ = os.environ.copy()
|
||||
environ.update(self.env)
|
||||
else:
|
||||
environ = None
|
||||
try:
|
||||
c = subprocess.Popen(
|
||||
'{} "{}"'.format(editor, filename), env=environ, shell=True,
|
||||
)
|
||||
exit_code = c.wait()
|
||||
if exit_code != 0:
|
||||
raise ClickException("{}: Editing failed!".format(editor))
|
||||
except OSError as e:
|
||||
raise ClickException("{}: Editing failed: {}".format(editor, e))
|
||||
|
||||
def edit(self, text):
|
||||
import tempfile
|
||||
|
||||
text = text or ""
|
||||
if text and not text.endswith("\n"):
|
||||
text += "\n"
|
||||
|
||||
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
|
||||
try:
|
||||
if WIN:
|
||||
encoding = "utf-8-sig"
|
||||
text = text.replace("\n", "\r\n")
|
||||
else:
|
||||
encoding = "utf-8"
|
||||
text = text.encode(encoding)
|
||||
|
||||
f = os.fdopen(fd, "wb")
|
||||
f.write(text)
|
||||
f.close()
|
||||
timestamp = os.path.getmtime(name)
|
||||
|
||||
self.edit_file(name)
|
||||
|
||||
if self.require_save and os.path.getmtime(name) == timestamp:
|
||||
return None
|
||||
|
||||
f = open(name, "rb")
|
||||
try:
|
||||
rv = f.read()
|
||||
finally:
|
||||
f.close()
|
||||
return rv.decode("utf-8-sig").replace("\r\n", "\n")
|
||||
finally:
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
def open_url(url, wait=False, locate=False):
|
||||
import subprocess
|
||||
|
||||
def _unquote_file(url):
|
||||
try:
|
||||
import urllib
|
||||
except ImportError:
|
||||
import urllib
|
||||
if url.startswith("file://"):
|
||||
url = urllib.unquote(url[7:])
|
||||
return url
|
||||
|
||||
if sys.platform == "darwin":
|
||||
args = ["open"]
|
||||
if wait:
|
||||
args.append("-W")
|
||||
if locate:
|
||||
args.append("-R")
|
||||
args.append(_unquote_file(url))
|
||||
null = open("/dev/null", "w")
|
||||
try:
|
||||
return subprocess.Popen(args, stderr=null).wait()
|
||||
finally:
|
||||
null.close()
|
||||
elif WIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', "")))
|
||||
else:
|
||||
args = 'start {} "" "{}"'.format(
|
||||
"/WAIT" if wait else "", url.replace('"', "")
|
||||
)
|
||||
return os.system(args)
|
||||
elif CYGWIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', ""))
|
||||
else:
|
||||
args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', ""))
|
||||
return os.system(args)
|
||||
|
||||
try:
|
||||
if locate:
|
||||
url = os.path.dirname(_unquote_file(url)) or "."
|
||||
else:
|
||||
url = _unquote_file(url)
|
||||
c = subprocess.Popen(["xdg-open", url])
|
||||
if wait:
|
||||
return c.wait()
|
||||
return 0
|
||||
except OSError:
|
||||
if url.startswith(("http://", "https://")) and not locate and not wait:
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(url)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
def _translate_ch_to_exc(ch):
|
||||
if ch == u"\x03":
|
||||
raise KeyboardInterrupt()
|
||||
if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D
|
||||
raise EOFError()
|
||||
if ch == u"\x1a" and WIN: # Windows, Ctrl+Z
|
||||
raise EOFError()
|
||||
|
||||
|
||||
if WIN:
|
||||
import msvcrt
|
||||
|
||||
@contextlib.contextmanager
|
||||
def raw_terminal():
|
||||
yield
|
||||
|
||||
def getchar(echo):
|
||||
# The function `getch` will return a bytes object corresponding to
|
||||
# the pressed character. Since Windows 10 build 1803, it will also
|
||||
# return \x00 when called a second time after pressing a regular key.
|
||||
#
|
||||
# `getwch` does not share this probably-bugged behavior. Moreover, it
|
||||
# returns a Unicode object by default, which is what we want.
|
||||
#
|
||||
# Either of these functions will return \x00 or \xe0 to indicate
|
||||
# a special key, and you need to call the same function again to get
|
||||
# the "rest" of the code. The fun part is that \u00e0 is
|
||||
# "latin small letter a with grave", so if you type that on a French
|
||||
# keyboard, you _also_ get a \xe0.
|
||||
# E.g., consider the Up arrow. This returns \xe0 and then \x48. The
|
||||
# resulting Unicode string reads as "a with grave" + "capital H".
|
||||
# This is indistinguishable from when the user actually types
|
||||
# "a with grave" and then "capital H".
|
||||
#
|
||||
# When \xe0 is returned, we assume it's part of a special-key sequence
|
||||
# and call `getwch` again, but that means that when the user types
|
||||
# the \u00e0 character, `getchar` doesn't return until a second
|
||||
# character is typed.
|
||||
# The alternative is returning immediately, but that would mess up
|
||||
# cross-platform handling of arrow keys and others that start with
|
||||
# \xe0. Another option is using `getch`, but then we can't reliably
|
||||
# read non-ASCII characters, because return values of `getch` are
|
||||
# limited to the current 8-bit codepage.
|
||||
#
|
||||
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
|
||||
# is doing the right thing in more situations than with `getch`.
|
||||
if echo:
|
||||
func = msvcrt.getwche
|
||||
else:
|
||||
func = msvcrt.getwch
|
||||
|
||||
rv = func()
|
||||
if rv in (u"\x00", u"\xe0"):
|
||||
# \x00 and \xe0 are control characters that indicate special key,
|
||||
# see above.
|
||||
rv += func()
|
||||
_translate_ch_to_exc(rv)
|
||||
return rv
|
||||
|
||||
|
||||
else:
|
||||
import tty
|
||||
import termios
|
||||
|
||||
@contextlib.contextmanager
|
||||
def raw_terminal():
|
||||
if not isatty(sys.stdin):
|
||||
f = open("/dev/tty")
|
||||
fd = f.fileno()
|
||||
else:
|
||||
fd = sys.stdin.fileno()
|
||||
f = None
|
||||
try:
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
yield fd
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
sys.stdout.flush()
|
||||
if f is not None:
|
||||
f.close()
|
||||
except termios.error:
|
||||
pass
|
||||
|
||||
def getchar(echo):
|
||||
with raw_terminal() as fd:
|
||||
ch = os.read(fd, 32)
|
||||
ch = ch.decode(get_best_encoding(sys.stdin), "replace")
|
||||
if echo and isatty(sys.stdout):
|
||||
sys.stdout.write(ch)
|
||||
_translate_ch_to_exc(ch)
|
||||
return ch
|
||||
@@ -1,37 +0,0 @@
|
||||
import textwrap
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
class TextWrapper(textwrap.TextWrapper):
|
||||
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
|
||||
space_left = max(width - cur_len, 1)
|
||||
|
||||
if self.break_long_words:
|
||||
last = reversed_chunks[-1]
|
||||
cut = last[:space_left]
|
||||
res = last[space_left:]
|
||||
cur_line.append(cut)
|
||||
reversed_chunks[-1] = res
|
||||
elif not cur_line:
|
||||
cur_line.append(reversed_chunks.pop())
|
||||
|
||||
@contextmanager
|
||||
def extra_indent(self, indent):
|
||||
old_initial_indent = self.initial_indent
|
||||
old_subsequent_indent = self.subsequent_indent
|
||||
self.initial_indent += indent
|
||||
self.subsequent_indent += indent
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.initial_indent = old_initial_indent
|
||||
self.subsequent_indent = old_subsequent_indent
|
||||
|
||||
def indent_only(self, text):
|
||||
rv = []
|
||||
for idx, line in enumerate(text.splitlines()):
|
||||
indent = self.initial_indent
|
||||
if idx > 0:
|
||||
indent = self.subsequent_indent
|
||||
rv.append(indent + line)
|
||||
return "\n".join(rv)
|
||||
@@ -1,131 +0,0 @@
|
||||
import codecs
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ._compat import PY2
|
||||
|
||||
|
||||
def _find_unicode_literals_frame():
|
||||
import __future__
|
||||
|
||||
if not hasattr(sys, "_getframe"): # not all Python implementations have it
|
||||
return 0
|
||||
frm = sys._getframe(1)
|
||||
idx = 1
|
||||
while frm is not None:
|
||||
if frm.f_globals.get("__name__", "").startswith("click."):
|
||||
frm = frm.f_back
|
||||
idx += 1
|
||||
elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag:
|
||||
return idx
|
||||
else:
|
||||
break
|
||||
return 0
|
||||
|
||||
|
||||
def _check_for_unicode_literals():
|
||||
if not __debug__:
|
||||
return
|
||||
|
||||
from . import disable_unicode_literals_warning
|
||||
|
||||
if not PY2 or disable_unicode_literals_warning:
|
||||
return
|
||||
bad_frame = _find_unicode_literals_frame()
|
||||
if bad_frame <= 0:
|
||||
return
|
||||
from warnings import warn
|
||||
|
||||
warn(
|
||||
Warning(
|
||||
"Click detected the use of the unicode_literals __future__"
|
||||
" import. This is heavily discouraged because it can"
|
||||
" introduce subtle bugs in your code. You should instead"
|
||||
' use explicit u"" literals for your unicode strings. For'
|
||||
" more information see"
|
||||
" https://click.palletsprojects.com/python3/"
|
||||
),
|
||||
stacklevel=bad_frame,
|
||||
)
|
||||
|
||||
|
||||
def _verify_python3_env():
|
||||
"""Ensures that the environment is good for unicode on Python 3."""
|
||||
if PY2:
|
||||
return
|
||||
try:
|
||||
import locale
|
||||
|
||||
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
|
||||
except Exception:
|
||||
fs_enc = "ascii"
|
||||
if fs_enc != "ascii":
|
||||
return
|
||||
|
||||
extra = ""
|
||||
if os.name == "posix":
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
rv = subprocess.Popen(
|
||||
["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
).communicate()[0]
|
||||
except OSError:
|
||||
rv = b""
|
||||
good_locales = set()
|
||||
has_c_utf8 = False
|
||||
|
||||
# Make sure we're operating on text here.
|
||||
if isinstance(rv, bytes):
|
||||
rv = rv.decode("ascii", "replace")
|
||||
|
||||
for line in rv.splitlines():
|
||||
locale = line.strip()
|
||||
if locale.lower().endswith((".utf-8", ".utf8")):
|
||||
good_locales.add(locale)
|
||||
if locale.lower() in ("c.utf8", "c.utf-8"):
|
||||
has_c_utf8 = True
|
||||
|
||||
extra += "\n\n"
|
||||
if not good_locales:
|
||||
extra += (
|
||||
"Additional information: on this system no suitable"
|
||||
" UTF-8 locales were discovered. This most likely"
|
||||
" requires resolving by reconfiguring the locale"
|
||||
" system."
|
||||
)
|
||||
elif has_c_utf8:
|
||||
extra += (
|
||||
"This system supports the C.UTF-8 locale which is"
|
||||
" recommended. You might be able to resolve your issue"
|
||||
" by exporting the following environment variables:\n\n"
|
||||
" export LC_ALL=C.UTF-8\n"
|
||||
" export LANG=C.UTF-8"
|
||||
)
|
||||
else:
|
||||
extra += (
|
||||
"This system lists a couple of UTF-8 supporting locales"
|
||||
" that you can pick from. The following suitable"
|
||||
" locales were discovered: {}".format(", ".join(sorted(good_locales)))
|
||||
)
|
||||
|
||||
bad_locale = None
|
||||
for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
|
||||
if locale and locale.lower().endswith((".utf-8", ".utf8")):
|
||||
bad_locale = locale
|
||||
if locale is not None:
|
||||
break
|
||||
if bad_locale is not None:
|
||||
extra += (
|
||||
"\n\nClick discovered that you exported a UTF-8 locale"
|
||||
" but the locale system could not pick up from it"
|
||||
" because it does not exist. The exported locale is"
|
||||
" '{}' but it is not supported".format(bad_locale)
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
"Click will abort further execution because Python 3 was"
|
||||
" configured to use ASCII as encoding for the environment."
|
||||
" Consult https://click.palletsprojects.com/python3/ for"
|
||||
" mitigation steps.{}".format(extra)
|
||||
)
|
||||
@@ -1,370 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This module is based on the excellent work by Adam Bartoš who
|
||||
# provided a lot of what went into the implementation here in
|
||||
# the discussion to issue1602 in the Python bug tracker.
|
||||
#
|
||||
# There are some general differences in regards to how this works
|
||||
# compared to the original patches as we do not need to patch
|
||||
# the entire interpreter but just work in our little world of
|
||||
# echo and prmopt.
|
||||
import ctypes
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
from ctypes import byref
|
||||
from ctypes import c_char
|
||||
from ctypes import c_char_p
|
||||
from ctypes import c_int
|
||||
from ctypes import c_ssize_t
|
||||
from ctypes import c_ulong
|
||||
from ctypes import c_void_p
|
||||
from ctypes import POINTER
|
||||
from ctypes import py_object
|
||||
from ctypes import windll
|
||||
from ctypes import WinError
|
||||
from ctypes import WINFUNCTYPE
|
||||
from ctypes.wintypes import DWORD
|
||||
from ctypes.wintypes import HANDLE
|
||||
from ctypes.wintypes import LPCWSTR
|
||||
from ctypes.wintypes import LPWSTR
|
||||
|
||||
import msvcrt
|
||||
|
||||
from ._compat import _NonClosingTextIOWrapper
|
||||
from ._compat import PY2
|
||||
from ._compat import text_type
|
||||
|
||||
try:
|
||||
from ctypes import pythonapi
|
||||
|
||||
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
|
||||
PyBuffer_Release = pythonapi.PyBuffer_Release
|
||||
except ImportError:
|
||||
pythonapi = None
|
||||
|
||||
|
||||
c_ssize_p = POINTER(c_ssize_t)
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
GetStdHandle = kernel32.GetStdHandle
|
||||
ReadConsoleW = kernel32.ReadConsoleW
|
||||
WriteConsoleW = kernel32.WriteConsoleW
|
||||
GetConsoleMode = kernel32.GetConsoleMode
|
||||
GetLastError = kernel32.GetLastError
|
||||
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
|
||||
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
||||
("CommandLineToArgvW", windll.shell32)
|
||||
)
|
||||
LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(
|
||||
("LocalFree", windll.kernel32)
|
||||
)
|
||||
|
||||
|
||||
STDIN_HANDLE = GetStdHandle(-10)
|
||||
STDOUT_HANDLE = GetStdHandle(-11)
|
||||
STDERR_HANDLE = GetStdHandle(-12)
|
||||
|
||||
|
||||
PyBUF_SIMPLE = 0
|
||||
PyBUF_WRITABLE = 1
|
||||
|
||||
ERROR_SUCCESS = 0
|
||||
ERROR_NOT_ENOUGH_MEMORY = 8
|
||||
ERROR_OPERATION_ABORTED = 995
|
||||
|
||||
STDIN_FILENO = 0
|
||||
STDOUT_FILENO = 1
|
||||
STDERR_FILENO = 2
|
||||
|
||||
EOF = b"\x1a"
|
||||
MAX_BYTES_WRITTEN = 32767
|
||||
|
||||
|
||||
class Py_buffer(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("buf", c_void_p),
|
||||
("obj", py_object),
|
||||
("len", c_ssize_t),
|
||||
("itemsize", c_ssize_t),
|
||||
("readonly", c_int),
|
||||
("ndim", c_int),
|
||||
("format", c_char_p),
|
||||
("shape", c_ssize_p),
|
||||
("strides", c_ssize_p),
|
||||
("suboffsets", c_ssize_p),
|
||||
("internal", c_void_p),
|
||||
]
|
||||
|
||||
if PY2:
|
||||
_fields_.insert(-1, ("smalltable", c_ssize_t * 2))
|
||||
|
||||
|
||||
# On PyPy we cannot get buffers so our ability to operate here is
|
||||
# serverly limited.
|
||||
if pythonapi is None:
|
||||
get_buffer = None
|
||||
else:
|
||||
|
||||
def get_buffer(obj, writable=False):
|
||||
buf = Py_buffer()
|
||||
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
||||
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
||||
try:
|
||||
buffer_type = c_char * buf.len
|
||||
return buffer_type.from_address(buf.buf)
|
||||
finally:
|
||||
PyBuffer_Release(byref(buf))
|
||||
|
||||
|
||||
class _WindowsConsoleRawIOBase(io.RawIOBase):
|
||||
def __init__(self, handle):
|
||||
self.handle = handle
|
||||
|
||||
def isatty(self):
|
||||
io.RawIOBase.isatty(self)
|
||||
return True
|
||||
|
||||
|
||||
class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def readinto(self, b):
|
||||
bytes_to_be_read = len(b)
|
||||
if not bytes_to_be_read:
|
||||
return 0
|
||||
elif bytes_to_be_read % 2:
|
||||
raise ValueError(
|
||||
"cannot read odd number of bytes from UTF-16-LE encoded console"
|
||||
)
|
||||
|
||||
buffer = get_buffer(b, writable=True)
|
||||
code_units_to_be_read = bytes_to_be_read // 2
|
||||
code_units_read = c_ulong()
|
||||
|
||||
rv = ReadConsoleW(
|
||||
HANDLE(self.handle),
|
||||
buffer,
|
||||
code_units_to_be_read,
|
||||
byref(code_units_read),
|
||||
None,
|
||||
)
|
||||
if GetLastError() == ERROR_OPERATION_ABORTED:
|
||||
# wait for KeyboardInterrupt
|
||||
time.sleep(0.1)
|
||||
if not rv:
|
||||
raise OSError("Windows error: {}".format(GetLastError()))
|
||||
|
||||
if buffer[0] == EOF:
|
||||
return 0
|
||||
return 2 * code_units_read.value
|
||||
|
||||
|
||||
class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
||||
def writable(self):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _get_error_message(errno):
|
||||
if errno == ERROR_SUCCESS:
|
||||
return "ERROR_SUCCESS"
|
||||
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
||||
return "ERROR_NOT_ENOUGH_MEMORY"
|
||||
return "Windows error {}".format(errno)
|
||||
|
||||
def write(self, b):
|
||||
bytes_to_be_written = len(b)
|
||||
buf = get_buffer(b)
|
||||
code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
|
||||
code_units_written = c_ulong()
|
||||
|
||||
WriteConsoleW(
|
||||
HANDLE(self.handle),
|
||||
buf,
|
||||
code_units_to_be_written,
|
||||
byref(code_units_written),
|
||||
None,
|
||||
)
|
||||
bytes_written = 2 * code_units_written.value
|
||||
|
||||
if bytes_written == 0 and bytes_to_be_written > 0:
|
||||
raise OSError(self._get_error_message(GetLastError()))
|
||||
return bytes_written
|
||||
|
||||
|
||||
class ConsoleStream(object):
|
||||
def __init__(self, text_stream, byte_stream):
|
||||
self._text_stream = text_stream
|
||||
self.buffer = byte_stream
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.buffer.name
|
||||
|
||||
def write(self, x):
|
||||
if isinstance(x, text_type):
|
||||
return self._text_stream.write(x)
|
||||
try:
|
||||
self.flush()
|
||||
except Exception:
|
||||
pass
|
||||
return self.buffer.write(x)
|
||||
|
||||
def writelines(self, lines):
|
||||
for line in lines:
|
||||
self.write(line)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._text_stream, name)
|
||||
|
||||
def isatty(self):
|
||||
return self.buffer.isatty()
|
||||
|
||||
def __repr__(self):
|
||||
return "<ConsoleStream name={!r} encoding={!r}>".format(
|
||||
self.name, self.encoding
|
||||
)
|
||||
|
||||
|
||||
class WindowsChunkedWriter(object):
|
||||
"""
|
||||
Wraps a stream (such as stdout), acting as a transparent proxy for all
|
||||
attribute access apart from method 'write()' which we wrap to write in
|
||||
limited chunks due to a Windows limitation on binary console streams.
|
||||
"""
|
||||
|
||||
def __init__(self, wrapped):
|
||||
# double-underscore everything to prevent clashes with names of
|
||||
# attributes on the wrapped stream object.
|
||||
self.__wrapped = wrapped
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.__wrapped, name)
|
||||
|
||||
def write(self, text):
|
||||
total_to_write = len(text)
|
||||
written = 0
|
||||
|
||||
while written < total_to_write:
|
||||
to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)
|
||||
self.__wrapped.write(text[written : written + to_write])
|
||||
written += to_write
|
||||
|
||||
|
||||
_wrapped_std_streams = set()
|
||||
|
||||
|
||||
def _wrap_std_stream(name):
|
||||
# Python 2 & Windows 7 and below
|
||||
if (
|
||||
PY2
|
||||
and sys.getwindowsversion()[:2] <= (6, 1)
|
||||
and name not in _wrapped_std_streams
|
||||
):
|
||||
setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))
|
||||
_wrapped_std_streams.add(name)
|
||||
|
||||
|
||||
def _get_text_stdin(buffer_stream):
|
||||
text_stream = _NonClosingTextIOWrapper(
|
||||
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
line_buffering=True,
|
||||
)
|
||||
return ConsoleStream(text_stream, buffer_stream)
|
||||
|
||||
|
||||
def _get_text_stdout(buffer_stream):
|
||||
text_stream = _NonClosingTextIOWrapper(
|
||||
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
line_buffering=True,
|
||||
)
|
||||
return ConsoleStream(text_stream, buffer_stream)
|
||||
|
||||
|
||||
def _get_text_stderr(buffer_stream):
|
||||
text_stream = _NonClosingTextIOWrapper(
|
||||
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
line_buffering=True,
|
||||
)
|
||||
return ConsoleStream(text_stream, buffer_stream)
|
||||
|
||||
|
||||
if PY2:
|
||||
|
||||
def _hash_py_argv():
|
||||
return zlib.crc32("\x00".join(sys.argv[1:]))
|
||||
|
||||
_initial_argv_hash = _hash_py_argv()
|
||||
|
||||
def _get_windows_argv():
|
||||
argc = c_int(0)
|
||||
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
|
||||
if not argv_unicode:
|
||||
raise WinError()
|
||||
try:
|
||||
argv = [argv_unicode[i] for i in range(0, argc.value)]
|
||||
finally:
|
||||
LocalFree(argv_unicode)
|
||||
del argv_unicode
|
||||
|
||||
if not hasattr(sys, "frozen"):
|
||||
argv = argv[1:]
|
||||
while len(argv) > 0:
|
||||
arg = argv[0]
|
||||
if not arg.startswith("-") or arg == "-":
|
||||
break
|
||||
argv = argv[1:]
|
||||
if arg.startswith(("-c", "-m")):
|
||||
break
|
||||
|
||||
return argv[1:]
|
||||
|
||||
|
||||
_stream_factories = {
|
||||
0: _get_text_stdin,
|
||||
1: _get_text_stdout,
|
||||
2: _get_text_stderr,
|
||||
}
|
||||
|
||||
|
||||
def _is_console(f):
|
||||
if not hasattr(f, "fileno"):
|
||||
return False
|
||||
|
||||
try:
|
||||
fileno = f.fileno()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
handle = msvcrt.get_osfhandle(fileno)
|
||||
return bool(GetConsoleMode(handle, byref(DWORD())))
|
||||
|
||||
|
||||
def _get_windows_console_stream(f, encoding, errors):
|
||||
if (
|
||||
get_buffer is not None
|
||||
and encoding in ("utf-16-le", None)
|
||||
and errors in ("strict", None)
|
||||
and _is_console(f)
|
||||
):
|
||||
func = _stream_factories.get(f.fileno())
|
||||
if func is not None:
|
||||
if not PY2:
|
||||
f = getattr(f, "buffer", None)
|
||||
if f is None:
|
||||
return None
|
||||
else:
|
||||
# If we are on Python 2 we need to set the stream that we
|
||||
# deal with to binary mode as otherwise the exercise if a
|
||||
# bit moot. The same problems apply as for
|
||||
# get_binary_stdin and friends from _compat.
|
||||
msvcrt.setmode(f.fileno(), os.O_BINARY)
|
||||
return func(f)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,333 +0,0 @@
|
||||
import inspect
|
||||
import sys
|
||||
from functools import update_wrapper
|
||||
|
||||
from ._compat import iteritems
|
||||
from ._unicodefun import _check_for_unicode_literals
|
||||
from .core import Argument
|
||||
from .core import Command
|
||||
from .core import Group
|
||||
from .core import Option
|
||||
from .globals import get_current_context
|
||||
from .utils import echo
|
||||
|
||||
|
||||
def pass_context(f):
|
||||
"""Marks a callback as wanting to receive the current context
|
||||
object as first argument.
|
||||
"""
|
||||
|
||||
def new_func(*args, **kwargs):
|
||||
return f(get_current_context(), *args, **kwargs)
|
||||
|
||||
return update_wrapper(new_func, f)
|
||||
|
||||
|
||||
def pass_obj(f):
|
||||
"""Similar to :func:`pass_context`, but only pass the object on the
|
||||
context onwards (:attr:`Context.obj`). This is useful if that object
|
||||
represents the state of a nested system.
|
||||
"""
|
||||
|
||||
def new_func(*args, **kwargs):
|
||||
return f(get_current_context().obj, *args, **kwargs)
|
||||
|
||||
return update_wrapper(new_func, f)
|
||||
|
||||
|
||||
def make_pass_decorator(object_type, ensure=False):
|
||||
"""Given an object type this creates a decorator that will work
|
||||
similar to :func:`pass_obj` but instead of passing the object of the
|
||||
current context, it will find the innermost context of type
|
||||
:func:`object_type`.
|
||||
|
||||
This generates a decorator that works roughly like this::
|
||||
|
||||
from functools import update_wrapper
|
||||
|
||||
def decorator(f):
|
||||
@pass_context
|
||||
def new_func(ctx, *args, **kwargs):
|
||||
obj = ctx.find_object(object_type)
|
||||
return ctx.invoke(f, obj, *args, **kwargs)
|
||||
return update_wrapper(new_func, f)
|
||||
return decorator
|
||||
|
||||
:param object_type: the type of the object to pass.
|
||||
:param ensure: if set to `True`, a new object will be created and
|
||||
remembered on the context if it's not there yet.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
def new_func(*args, **kwargs):
|
||||
ctx = get_current_context()
|
||||
if ensure:
|
||||
obj = ctx.ensure_object(object_type)
|
||||
else:
|
||||
obj = ctx.find_object(object_type)
|
||||
if obj is None:
|
||||
raise RuntimeError(
|
||||
"Managed to invoke callback without a context"
|
||||
" object of type '{}' existing".format(object_type.__name__)
|
||||
)
|
||||
return ctx.invoke(f, obj, *args, **kwargs)
|
||||
|
||||
return update_wrapper(new_func, f)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _make_command(f, name, attrs, cls):
|
||||
if isinstance(f, Command):
|
||||
raise TypeError("Attempted to convert a callback into a command twice.")
|
||||
try:
|
||||
params = f.__click_params__
|
||||
params.reverse()
|
||||
del f.__click_params__
|
||||
except AttributeError:
|
||||
params = []
|
||||
help = attrs.get("help")
|
||||
if help is None:
|
||||
help = inspect.getdoc(f)
|
||||
if isinstance(help, bytes):
|
||||
help = help.decode("utf-8")
|
||||
else:
|
||||
help = inspect.cleandoc(help)
|
||||
attrs["help"] = help
|
||||
_check_for_unicode_literals()
|
||||
return cls(
|
||||
name=name or f.__name__.lower().replace("_", "-"),
|
||||
callback=f,
|
||||
params=params,
|
||||
**attrs
|
||||
)
|
||||
|
||||
|
||||
def command(name=None, cls=None, **attrs):
|
||||
r"""Creates a new :class:`Command` and uses the decorated function as
|
||||
callback. This will also automatically attach all decorated
|
||||
:func:`option`\s and :func:`argument`\s as parameters to the command.
|
||||
|
||||
The name of the command defaults to the name of the function with
|
||||
underscores replaced by dashes. If you want to change that, you can
|
||||
pass the intended name as the first argument.
|
||||
|
||||
All keyword arguments are forwarded to the underlying command class.
|
||||
|
||||
Once decorated the function turns into a :class:`Command` instance
|
||||
that can be invoked as a command line utility or be attached to a
|
||||
command :class:`Group`.
|
||||
|
||||
:param name: the name of the command. This defaults to the function
|
||||
name with underscores replaced by dashes.
|
||||
:param cls: the command class to instantiate. This defaults to
|
||||
:class:`Command`.
|
||||
"""
|
||||
if cls is None:
|
||||
cls = Command
|
||||
|
||||
def decorator(f):
|
||||
cmd = _make_command(f, name, attrs, cls)
|
||||
cmd.__doc__ = f.__doc__
|
||||
return cmd
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def group(name=None, **attrs):
|
||||
"""Creates a new :class:`Group` with a function as callback. This
|
||||
works otherwise the same as :func:`command` just that the `cls`
|
||||
parameter is set to :class:`Group`.
|
||||
"""
|
||||
attrs.setdefault("cls", Group)
|
||||
return command(name, **attrs)
|
||||
|
||||
|
||||
def _param_memo(f, param):
|
||||
if isinstance(f, Command):
|
||||
f.params.append(param)
|
||||
else:
|
||||
if not hasattr(f, "__click_params__"):
|
||||
f.__click_params__ = []
|
||||
f.__click_params__.append(param)
|
||||
|
||||
|
||||
def argument(*param_decls, **attrs):
|
||||
"""Attaches an argument to the command. All positional arguments are
|
||||
passed as parameter declarations to :class:`Argument`; all keyword
|
||||
arguments are forwarded unchanged (except ``cls``).
|
||||
This is equivalent to creating an :class:`Argument` instance manually
|
||||
and attaching it to the :attr:`Command.params` list.
|
||||
|
||||
:param cls: the argument class to instantiate. This defaults to
|
||||
:class:`Argument`.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
ArgumentClass = attrs.pop("cls", Argument)
|
||||
_param_memo(f, ArgumentClass(param_decls, **attrs))
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def option(*param_decls, **attrs):
|
||||
"""Attaches an option to the command. All positional arguments are
|
||||
passed as parameter declarations to :class:`Option`; all keyword
|
||||
arguments are forwarded unchanged (except ``cls``).
|
||||
This is equivalent to creating an :class:`Option` instance manually
|
||||
and attaching it to the :attr:`Command.params` list.
|
||||
|
||||
:param cls: the option class to instantiate. This defaults to
|
||||
:class:`Option`.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
# Issue 926, copy attrs, so pre-defined options can re-use the same cls=
|
||||
option_attrs = attrs.copy()
|
||||
|
||||
if "help" in option_attrs:
|
||||
option_attrs["help"] = inspect.cleandoc(option_attrs["help"])
|
||||
OptionClass = option_attrs.pop("cls", Option)
|
||||
_param_memo(f, OptionClass(param_decls, **option_attrs))
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def confirmation_option(*param_decls, **attrs):
|
||||
"""Shortcut for confirmation prompts that can be ignored by passing
|
||||
``--yes`` as parameter.
|
||||
|
||||
This is equivalent to decorating a function with :func:`option` with
|
||||
the following parameters::
|
||||
|
||||
def callback(ctx, param, value):
|
||||
if not value:
|
||||
ctx.abort()
|
||||
|
||||
@click.command()
|
||||
@click.option('--yes', is_flag=True, callback=callback,
|
||||
expose_value=False, prompt='Do you want to continue?')
|
||||
def dropdb():
|
||||
pass
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
def callback(ctx, param, value):
|
||||
if not value:
|
||||
ctx.abort()
|
||||
|
||||
attrs.setdefault("is_flag", True)
|
||||
attrs.setdefault("callback", callback)
|
||||
attrs.setdefault("expose_value", False)
|
||||
attrs.setdefault("prompt", "Do you want to continue?")
|
||||
attrs.setdefault("help", "Confirm the action without prompting.")
|
||||
return option(*(param_decls or ("--yes",)), **attrs)(f)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def password_option(*param_decls, **attrs):
|
||||
"""Shortcut for password prompts.
|
||||
|
||||
This is equivalent to decorating a function with :func:`option` with
|
||||
the following parameters::
|
||||
|
||||
@click.command()
|
||||
@click.option('--password', prompt=True, confirmation_prompt=True,
|
||||
hide_input=True)
|
||||
def changeadmin(password):
|
||||
pass
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
attrs.setdefault("prompt", True)
|
||||
attrs.setdefault("confirmation_prompt", True)
|
||||
attrs.setdefault("hide_input", True)
|
||||
return option(*(param_decls or ("--password",)), **attrs)(f)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def version_option(version=None, *param_decls, **attrs):
|
||||
"""Adds a ``--version`` option which immediately ends the program
|
||||
printing out the version number. This is implemented as an eager
|
||||
option that prints the version and exits the program in the callback.
|
||||
|
||||
:param version: the version number to show. If not provided Click
|
||||
attempts an auto discovery via setuptools.
|
||||
:param prog_name: the name of the program (defaults to autodetection)
|
||||
:param message: custom message to show instead of the default
|
||||
(``'%(prog)s, version %(version)s'``)
|
||||
:param others: everything else is forwarded to :func:`option`.
|
||||
"""
|
||||
if version is None:
|
||||
if hasattr(sys, "_getframe"):
|
||||
module = sys._getframe(1).f_globals.get("__name__")
|
||||
else:
|
||||
module = ""
|
||||
|
||||
def decorator(f):
|
||||
prog_name = attrs.pop("prog_name", None)
|
||||
message = attrs.pop("message", "%(prog)s, version %(version)s")
|
||||
|
||||
def callback(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
prog = prog_name
|
||||
if prog is None:
|
||||
prog = ctx.find_root().info_name
|
||||
ver = version
|
||||
if ver is None:
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
for dist in pkg_resources.working_set:
|
||||
scripts = dist.get_entry_map().get("console_scripts") or {}
|
||||
for _, entry_point in iteritems(scripts):
|
||||
if entry_point.module_name == module:
|
||||
ver = dist.version
|
||||
break
|
||||
if ver is None:
|
||||
raise RuntimeError("Could not determine version")
|
||||
echo(message % {"prog": prog, "version": ver}, color=ctx.color)
|
||||
ctx.exit()
|
||||
|
||||
attrs.setdefault("is_flag", True)
|
||||
attrs.setdefault("expose_value", False)
|
||||
attrs.setdefault("is_eager", True)
|
||||
attrs.setdefault("help", "Show the version and exit.")
|
||||
attrs["callback"] = callback
|
||||
return option(*(param_decls or ("--version",)), **attrs)(f)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def help_option(*param_decls, **attrs):
|
||||
"""Adds a ``--help`` option which immediately ends the program
|
||||
printing out the help page. This is usually unnecessary to add as
|
||||
this is added by default to all commands unless suppressed.
|
||||
|
||||
Like :func:`version_option`, this is implemented as eager option that
|
||||
prints in the callback and exits.
|
||||
|
||||
All arguments are forwarded to :func:`option`.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
def callback(ctx, param, value):
|
||||
if value and not ctx.resilient_parsing:
|
||||
echo(ctx.get_help(), color=ctx.color)
|
||||
ctx.exit()
|
||||
|
||||
attrs.setdefault("is_flag", True)
|
||||
attrs.setdefault("expose_value", False)
|
||||
attrs.setdefault("help", "Show this message and exit.")
|
||||
attrs.setdefault("is_eager", True)
|
||||
attrs["callback"] = callback
|
||||
return option(*(param_decls or ("--help",)), **attrs)(f)
|
||||
|
||||
return decorator
|
||||
@@ -1,253 +0,0 @@
|
||||
from ._compat import filename_to_ui
|
||||
from ._compat import get_text_stderr
|
||||
from ._compat import PY2
|
||||
from .utils import echo
|
||||
|
||||
|
||||
def _join_param_hints(param_hint):
|
||||
if isinstance(param_hint, (tuple, list)):
|
||||
return " / ".join(repr(x) for x in param_hint)
|
||||
return param_hint
|
||||
|
||||
|
||||
class ClickException(Exception):
|
||||
"""An exception that Click can handle and show to the user."""
|
||||
|
||||
#: The exit code for this exception
|
||||
exit_code = 1
|
||||
|
||||
def __init__(self, message):
|
||||
ctor_msg = message
|
||||
if PY2:
|
||||
if ctor_msg is not None:
|
||||
ctor_msg = ctor_msg.encode("utf-8")
|
||||
Exception.__init__(self, ctor_msg)
|
||||
self.message = message
|
||||
|
||||
def format_message(self):
|
||||
return self.message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
if PY2:
|
||||
__unicode__ = __str__
|
||||
|
||||
def __str__(self):
|
||||
return self.message.encode("utf-8")
|
||||
|
||||
def show(self, file=None):
|
||||
if file is None:
|
||||
file = get_text_stderr()
|
||||
echo("Error: {}".format(self.format_message()), file=file)
|
||||
|
||||
|
||||
class UsageError(ClickException):
|
||||
"""An internal exception that signals a usage error. This typically
|
||||
aborts any further handling.
|
||||
|
||||
:param message: the error message to display.
|
||||
:param ctx: optionally the context that caused this error. Click will
|
||||
fill in the context automatically in some situations.
|
||||
"""
|
||||
|
||||
exit_code = 2
|
||||
|
||||
def __init__(self, message, ctx=None):
|
||||
ClickException.__init__(self, message)
|
||||
self.ctx = ctx
|
||||
self.cmd = self.ctx.command if self.ctx else None
|
||||
|
||||
def show(self, file=None):
|
||||
if file is None:
|
||||
file = get_text_stderr()
|
||||
color = None
|
||||
hint = ""
|
||||
if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
|
||||
hint = "Try '{} {}' for help.\n".format(
|
||||
self.ctx.command_path, self.ctx.help_option_names[0]
|
||||
)
|
||||
if self.ctx is not None:
|
||||
color = self.ctx.color
|
||||
echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color)
|
||||
echo("Error: {}".format(self.format_message()), file=file, color=color)
|
||||
|
||||
|
||||
class BadParameter(UsageError):
|
||||
"""An exception that formats out a standardized error message for a
|
||||
bad parameter. This is useful when thrown from a callback or type as
|
||||
Click will attach contextual information to it (for instance, which
|
||||
parameter it is).
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
:param param: the parameter object that caused this error. This can
|
||||
be left out, and Click will attach this info itself
|
||||
if possible.
|
||||
:param param_hint: a string that shows up as parameter name. This
|
||||
can be used as alternative to `param` in cases
|
||||
where custom validation should happen. If it is
|
||||
a string it's used as such, if it's a list then
|
||||
each item is quoted and separated.
|
||||
"""
|
||||
|
||||
def __init__(self, message, ctx=None, param=None, param_hint=None):
|
||||
UsageError.__init__(self, message, ctx)
|
||||
self.param = param
|
||||
self.param_hint = param_hint
|
||||
|
||||
def format_message(self):
|
||||
if self.param_hint is not None:
|
||||
param_hint = self.param_hint
|
||||
elif self.param is not None:
|
||||
param_hint = self.param.get_error_hint(self.ctx)
|
||||
else:
|
||||
return "Invalid value: {}".format(self.message)
|
||||
param_hint = _join_param_hints(param_hint)
|
||||
|
||||
return "Invalid value for {}: {}".format(param_hint, self.message)
|
||||
|
||||
|
||||
class MissingParameter(BadParameter):
|
||||
"""Raised if click required an option or argument but it was not
|
||||
provided when invoking the script.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
|
||||
:param param_type: a string that indicates the type of the parameter.
|
||||
The default is to inherit the parameter type from
|
||||
the given `param`. Valid values are ``'parameter'``,
|
||||
``'option'`` or ``'argument'``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, message=None, ctx=None, param=None, param_hint=None, param_type=None
|
||||
):
|
||||
BadParameter.__init__(self, message, ctx, param, param_hint)
|
||||
self.param_type = param_type
|
||||
|
||||
def format_message(self):
|
||||
if self.param_hint is not None:
|
||||
param_hint = self.param_hint
|
||||
elif self.param is not None:
|
||||
param_hint = self.param.get_error_hint(self.ctx)
|
||||
else:
|
||||
param_hint = None
|
||||
param_hint = _join_param_hints(param_hint)
|
||||
|
||||
param_type = self.param_type
|
||||
if param_type is None and self.param is not None:
|
||||
param_type = self.param.param_type_name
|
||||
|
||||
msg = self.message
|
||||
if self.param is not None:
|
||||
msg_extra = self.param.type.get_missing_message(self.param)
|
||||
if msg_extra:
|
||||
if msg:
|
||||
msg += ". {}".format(msg_extra)
|
||||
else:
|
||||
msg = msg_extra
|
||||
|
||||
return "Missing {}{}{}{}".format(
|
||||
param_type,
|
||||
" {}".format(param_hint) if param_hint else "",
|
||||
". " if msg else ".",
|
||||
msg or "",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.message is None:
|
||||
param_name = self.param.name if self.param else None
|
||||
return "missing parameter: {}".format(param_name)
|
||||
else:
|
||||
return self.message
|
||||
|
||||
if PY2:
|
||||
__unicode__ = __str__
|
||||
|
||||
def __str__(self):
|
||||
return self.__unicode__().encode("utf-8")
|
||||
|
||||
|
||||
class NoSuchOption(UsageError):
|
||||
"""Raised if click attempted to handle an option that does not
|
||||
exist.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
"""
|
||||
|
||||
def __init__(self, option_name, message=None, possibilities=None, ctx=None):
|
||||
if message is None:
|
||||
message = "no such option: {}".format(option_name)
|
||||
UsageError.__init__(self, message, ctx)
|
||||
self.option_name = option_name
|
||||
self.possibilities = possibilities
|
||||
|
||||
def format_message(self):
|
||||
bits = [self.message]
|
||||
if self.possibilities:
|
||||
if len(self.possibilities) == 1:
|
||||
bits.append("Did you mean {}?".format(self.possibilities[0]))
|
||||
else:
|
||||
possibilities = sorted(self.possibilities)
|
||||
bits.append("(Possible options: {})".format(", ".join(possibilities)))
|
||||
return " ".join(bits)
|
||||
|
||||
|
||||
class BadOptionUsage(UsageError):
|
||||
"""Raised if an option is generally supplied but the use of the option
|
||||
was incorrect. This is for instance raised if the number of arguments
|
||||
for an option is not correct.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
|
||||
:param option_name: the name of the option being used incorrectly.
|
||||
"""
|
||||
|
||||
def __init__(self, option_name, message, ctx=None):
|
||||
UsageError.__init__(self, message, ctx)
|
||||
self.option_name = option_name
|
||||
|
||||
|
||||
class BadArgumentUsage(UsageError):
|
||||
"""Raised if an argument is generally supplied but the use of the argument
|
||||
was incorrect. This is for instance raised if the number of values
|
||||
for an argument is not correct.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
"""
|
||||
|
||||
def __init__(self, message, ctx=None):
|
||||
UsageError.__init__(self, message, ctx)
|
||||
|
||||
|
||||
class FileError(ClickException):
|
||||
"""Raised if a file cannot be opened."""
|
||||
|
||||
def __init__(self, filename, hint=None):
|
||||
ui_filename = filename_to_ui(filename)
|
||||
if hint is None:
|
||||
hint = "unknown error"
|
||||
ClickException.__init__(self, hint)
|
||||
self.ui_filename = ui_filename
|
||||
self.filename = filename
|
||||
|
||||
def format_message(self):
|
||||
return "Could not open file {}: {}".format(self.ui_filename, self.message)
|
||||
|
||||
|
||||
class Abort(RuntimeError):
|
||||
"""An internal signalling exception that signals Click to abort."""
|
||||
|
||||
|
||||
class Exit(RuntimeError):
|
||||
"""An exception that indicates that the application should exit with some
|
||||
status code.
|
||||
|
||||
:param code: the status code to exit with.
|
||||
"""
|
||||
|
||||
__slots__ = ("exit_code",)
|
||||
|
||||
def __init__(self, code=0):
|
||||
self.exit_code = code
|
||||
@@ -1,283 +0,0 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ._compat import term_len
|
||||
from .parser import split_opt
|
||||
from .termui import get_terminal_size
|
||||
|
||||
# Can force a width. This is used by the test system
|
||||
FORCED_WIDTH = None
|
||||
|
||||
|
||||
def measure_table(rows):
|
||||
widths = {}
|
||||
for row in rows:
|
||||
for idx, col in enumerate(row):
|
||||
widths[idx] = max(widths.get(idx, 0), term_len(col))
|
||||
return tuple(y for x, y in sorted(widths.items()))
|
||||
|
||||
|
||||
def iter_rows(rows, col_count):
|
||||
for row in rows:
|
||||
row = tuple(row)
|
||||
yield row + ("",) * (col_count - len(row))
|
||||
|
||||
|
||||
def wrap_text(
|
||||
text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False
|
||||
):
|
||||
"""A helper function that intelligently wraps text. By default, it
|
||||
assumes that it operates on a single paragraph of text but if the
|
||||
`preserve_paragraphs` parameter is provided it will intelligently
|
||||
handle paragraphs (defined by two empty lines).
|
||||
|
||||
If paragraphs are handled, a paragraph can be prefixed with an empty
|
||||
line containing the ``\\b`` character (``\\x08``) to indicate that
|
||||
no rewrapping should happen in that block.
|
||||
|
||||
:param text: the text that should be rewrapped.
|
||||
:param width: the maximum width for the text.
|
||||
:param initial_indent: the initial indent that should be placed on the
|
||||
first line as a string.
|
||||
:param subsequent_indent: the indent string that should be placed on
|
||||
each consecutive line.
|
||||
:param preserve_paragraphs: if this flag is set then the wrapping will
|
||||
intelligently handle paragraphs.
|
||||
"""
|
||||
from ._textwrap import TextWrapper
|
||||
|
||||
text = text.expandtabs()
|
||||
wrapper = TextWrapper(
|
||||
width,
|
||||
initial_indent=initial_indent,
|
||||
subsequent_indent=subsequent_indent,
|
||||
replace_whitespace=False,
|
||||
)
|
||||
if not preserve_paragraphs:
|
||||
return wrapper.fill(text)
|
||||
|
||||
p = []
|
||||
buf = []
|
||||
indent = None
|
||||
|
||||
def _flush_par():
|
||||
if not buf:
|
||||
return
|
||||
if buf[0].strip() == "\b":
|
||||
p.append((indent or 0, True, "\n".join(buf[1:])))
|
||||
else:
|
||||
p.append((indent or 0, False, " ".join(buf)))
|
||||
del buf[:]
|
||||
|
||||
for line in text.splitlines():
|
||||
if not line:
|
||||
_flush_par()
|
||||
indent = None
|
||||
else:
|
||||
if indent is None:
|
||||
orig_len = term_len(line)
|
||||
line = line.lstrip()
|
||||
indent = orig_len - term_len(line)
|
||||
buf.append(line)
|
||||
_flush_par()
|
||||
|
||||
rv = []
|
||||
for indent, raw, text in p:
|
||||
with wrapper.extra_indent(" " * indent):
|
||||
if raw:
|
||||
rv.append(wrapper.indent_only(text))
|
||||
else:
|
||||
rv.append(wrapper.fill(text))
|
||||
|
||||
return "\n\n".join(rv)
|
||||
|
||||
|
||||
class HelpFormatter(object):
|
||||
"""This class helps with formatting text-based help pages. It's
|
||||
usually just needed for very special internal cases, but it's also
|
||||
exposed so that developers can write their own fancy outputs.
|
||||
|
||||
At present, it always writes into memory.
|
||||
|
||||
:param indent_increment: the additional increment for each level.
|
||||
:param width: the width for the text. This defaults to the terminal
|
||||
width clamped to a maximum of 78.
|
||||
"""
|
||||
|
||||
def __init__(self, indent_increment=2, width=None, max_width=None):
|
||||
self.indent_increment = indent_increment
|
||||
if max_width is None:
|
||||
max_width = 80
|
||||
if width is None:
|
||||
width = FORCED_WIDTH
|
||||
if width is None:
|
||||
width = max(min(get_terminal_size()[0], max_width) - 2, 50)
|
||||
self.width = width
|
||||
self.current_indent = 0
|
||||
self.buffer = []
|
||||
|
||||
def write(self, string):
|
||||
"""Writes a unicode string into the internal buffer."""
|
||||
self.buffer.append(string)
|
||||
|
||||
def indent(self):
|
||||
"""Increases the indentation."""
|
||||
self.current_indent += self.indent_increment
|
||||
|
||||
def dedent(self):
|
||||
"""Decreases the indentation."""
|
||||
self.current_indent -= self.indent_increment
|
||||
|
||||
def write_usage(self, prog, args="", prefix="Usage: "):
|
||||
"""Writes a usage line into the buffer.
|
||||
|
||||
:param prog: the program name.
|
||||
:param args: whitespace separated list of arguments.
|
||||
:param prefix: the prefix for the first line.
|
||||
"""
|
||||
usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent)
|
||||
text_width = self.width - self.current_indent
|
||||
|
||||
if text_width >= (term_len(usage_prefix) + 20):
|
||||
# The arguments will fit to the right of the prefix.
|
||||
indent = " " * term_len(usage_prefix)
|
||||
self.write(
|
||||
wrap_text(
|
||||
args,
|
||||
text_width,
|
||||
initial_indent=usage_prefix,
|
||||
subsequent_indent=indent,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# The prefix is too long, put the arguments on the next line.
|
||||
self.write(usage_prefix)
|
||||
self.write("\n")
|
||||
indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
|
||||
self.write(
|
||||
wrap_text(
|
||||
args, text_width, initial_indent=indent, subsequent_indent=indent
|
||||
)
|
||||
)
|
||||
|
||||
self.write("\n")
|
||||
|
||||
def write_heading(self, heading):
|
||||
"""Writes a heading into the buffer."""
|
||||
self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent))
|
||||
|
||||
def write_paragraph(self):
|
||||
"""Writes a paragraph into the buffer."""
|
||||
if self.buffer:
|
||||
self.write("\n")
|
||||
|
||||
def write_text(self, text):
|
||||
"""Writes re-indented text into the buffer. This rewraps and
|
||||
preserves paragraphs.
|
||||
"""
|
||||
text_width = max(self.width - self.current_indent, 11)
|
||||
indent = " " * self.current_indent
|
||||
self.write(
|
||||
wrap_text(
|
||||
text,
|
||||
text_width,
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
preserve_paragraphs=True,
|
||||
)
|
||||
)
|
||||
self.write("\n")
|
||||
|
||||
def write_dl(self, rows, col_max=30, col_spacing=2):
|
||||
"""Writes a definition list into the buffer. This is how options
|
||||
and commands are usually formatted.
|
||||
|
||||
:param rows: a list of two item tuples for the terms and values.
|
||||
:param col_max: the maximum width of the first column.
|
||||
:param col_spacing: the number of spaces between the first and
|
||||
second column.
|
||||
"""
|
||||
rows = list(rows)
|
||||
widths = measure_table(rows)
|
||||
if len(widths) != 2:
|
||||
raise TypeError("Expected two columns for definition list")
|
||||
|
||||
first_col = min(widths[0], col_max) + col_spacing
|
||||
|
||||
for first, second in iter_rows(rows, len(widths)):
|
||||
self.write("{:>{w}}{}".format("", first, w=self.current_indent))
|
||||
if not second:
|
||||
self.write("\n")
|
||||
continue
|
||||
if term_len(first) <= first_col - col_spacing:
|
||||
self.write(" " * (first_col - term_len(first)))
|
||||
else:
|
||||
self.write("\n")
|
||||
self.write(" " * (first_col + self.current_indent))
|
||||
|
||||
text_width = max(self.width - first_col - 2, 10)
|
||||
wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
|
||||
lines = wrapped_text.splitlines()
|
||||
|
||||
if lines:
|
||||
self.write("{}\n".format(lines[0]))
|
||||
|
||||
for line in lines[1:]:
|
||||
self.write(
|
||||
"{:>{w}}{}\n".format(
|
||||
"", line, w=first_col + self.current_indent
|
||||
)
|
||||
)
|
||||
|
||||
if len(lines) > 1:
|
||||
# separate long help from next option
|
||||
self.write("\n")
|
||||
else:
|
||||
self.write("\n")
|
||||
|
||||
@contextmanager
|
||||
def section(self, name):
|
||||
"""Helpful context manager that writes a paragraph, a heading,
|
||||
and the indents.
|
||||
|
||||
:param name: the section name that is written as heading.
|
||||
"""
|
||||
self.write_paragraph()
|
||||
self.write_heading(name)
|
||||
self.indent()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.dedent()
|
||||
|
||||
@contextmanager
|
||||
def indentation(self):
|
||||
"""A context manager that increases the indentation."""
|
||||
self.indent()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.dedent()
|
||||
|
||||
def getvalue(self):
|
||||
"""Returns the buffer contents."""
|
||||
return "".join(self.buffer)
|
||||
|
||||
|
||||
def join_options(options):
|
||||
"""Given a list of option strings this joins them in the most appropriate
|
||||
way and returns them in the form ``(formatted_string,
|
||||
any_prefix_is_slash)`` where the second item in the tuple is a flag that
|
||||
indicates if any of the option prefixes was a slash.
|
||||
"""
|
||||
rv = []
|
||||
any_prefix_is_slash = False
|
||||
for opt in options:
|
||||
prefix = split_opt(opt)[0]
|
||||
if prefix == "/":
|
||||
any_prefix_is_slash = True
|
||||
rv.append((len(prefix), opt))
|
||||
|
||||
rv.sort(key=lambda x: x[0])
|
||||
|
||||
rv = ", ".join(x[1] for x in rv)
|
||||
return rv, any_prefix_is_slash
|
||||
@@ -1,47 +0,0 @@
|
||||
from threading import local
|
||||
|
||||
_local = local()
|
||||
|
||||
|
||||
def get_current_context(silent=False):
|
||||
"""Returns the current click context. This can be used as a way to
|
||||
access the current context object from anywhere. This is a more implicit
|
||||
alternative to the :func:`pass_context` decorator. This function is
|
||||
primarily useful for helpers such as :func:`echo` which might be
|
||||
interested in changing its behavior based on the current context.
|
||||
|
||||
To push the current context, :meth:`Context.scope` can be used.
|
||||
|
||||
.. versionadded:: 5.0
|
||||
|
||||
:param silent: if set to `True` the return value is `None` if no context
|
||||
is available. The default behavior is to raise a
|
||||
:exc:`RuntimeError`.
|
||||
"""
|
||||
try:
|
||||
return _local.stack[-1]
|
||||
except (AttributeError, IndexError):
|
||||
if not silent:
|
||||
raise RuntimeError("There is no active click context.")
|
||||
|
||||
|
||||
def push_context(ctx):
|
||||
"""Pushes a new context to the current stack."""
|
||||
_local.__dict__.setdefault("stack", []).append(ctx)
|
||||
|
||||
|
||||
def pop_context():
|
||||
"""Removes the top level from the stack."""
|
||||
_local.stack.pop()
|
||||
|
||||
|
||||
def resolve_color_default(color=None):
|
||||
""""Internal helper to get the default value of the color flag. If a
|
||||
value is passed it's returned unchanged, otherwise it's looked up from
|
||||
the current context.
|
||||
"""
|
||||
if color is not None:
|
||||
return color
|
||||
ctx = get_current_context(silent=True)
|
||||
if ctx is not None:
|
||||
return ctx.color
|
||||
@@ -1,428 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module started out as largely a copy paste from the stdlib's
|
||||
optparse module with the features removed that we do not need from
|
||||
optparse because we implement them in Click on a higher level (for
|
||||
instance type handling, help formatting and a lot more).
|
||||
|
||||
The plan is to remove more and more from here over time.
|
||||
|
||||
The reason this is a different module and not optparse from the stdlib
|
||||
is that there are differences in 2.x and 3.x about the error messages
|
||||
generated and optparse in the stdlib uses gettext for no good reason
|
||||
and might cause us issues.
|
||||
|
||||
Click uses parts of optparse written by Gregory P. Ward and maintained
|
||||
by the Python Software Foundation. This is limited to code in parser.py.
|
||||
|
||||
Copyright 2001-2006 Gregory P. Ward. All rights reserved.
|
||||
Copyright 2002-2006 Python Software Foundation. All rights reserved.
|
||||
"""
|
||||
import re
|
||||
from collections import deque
|
||||
|
||||
from .exceptions import BadArgumentUsage
|
||||
from .exceptions import BadOptionUsage
|
||||
from .exceptions import NoSuchOption
|
||||
from .exceptions import UsageError
|
||||
|
||||
|
||||
def _unpack_args(args, nargs_spec):
|
||||
"""Given an iterable of arguments and an iterable of nargs specifications,
|
||||
it returns a tuple with all the unpacked arguments at the first index
|
||||
and all remaining arguments as the second.
|
||||
|
||||
The nargs specification is the number of arguments that should be consumed
|
||||
or `-1` to indicate that this position should eat up all the remainders.
|
||||
|
||||
Missing items are filled with `None`.
|
||||
"""
|
||||
args = deque(args)
|
||||
nargs_spec = deque(nargs_spec)
|
||||
rv = []
|
||||
spos = None
|
||||
|
||||
def _fetch(c):
|
||||
try:
|
||||
if spos is None:
|
||||
return c.popleft()
|
||||
else:
|
||||
return c.pop()
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
while nargs_spec:
|
||||
nargs = _fetch(nargs_spec)
|
||||
if nargs == 1:
|
||||
rv.append(_fetch(args))
|
||||
elif nargs > 1:
|
||||
x = [_fetch(args) for _ in range(nargs)]
|
||||
# If we're reversed, we're pulling in the arguments in reverse,
|
||||
# so we need to turn them around.
|
||||
if spos is not None:
|
||||
x.reverse()
|
||||
rv.append(tuple(x))
|
||||
elif nargs < 0:
|
||||
if spos is not None:
|
||||
raise TypeError("Cannot have two nargs < 0")
|
||||
spos = len(rv)
|
||||
rv.append(None)
|
||||
|
||||
# spos is the position of the wildcard (star). If it's not `None`,
|
||||
# we fill it with the remainder.
|
||||
if spos is not None:
|
||||
rv[spos] = tuple(args)
|
||||
args = []
|
||||
rv[spos + 1 :] = reversed(rv[spos + 1 :])
|
||||
|
||||
return tuple(rv), list(args)
|
||||
|
||||
|
||||
def _error_opt_args(nargs, opt):
|
||||
if nargs == 1:
|
||||
raise BadOptionUsage(opt, "{} option requires an argument".format(opt))
|
||||
raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs))
|
||||
|
||||
|
||||
def split_opt(opt):
|
||||
first = opt[:1]
|
||||
if first.isalnum():
|
||||
return "", opt
|
||||
if opt[1:2] == first:
|
||||
return opt[:2], opt[2:]
|
||||
return first, opt[1:]
|
||||
|
||||
|
||||
def normalize_opt(opt, ctx):
|
||||
if ctx is None or ctx.token_normalize_func is None:
|
||||
return opt
|
||||
prefix, opt = split_opt(opt)
|
||||
return prefix + ctx.token_normalize_func(opt)
|
||||
|
||||
|
||||
def split_arg_string(string):
|
||||
"""Given an argument string this attempts to split it into small parts."""
|
||||
rv = []
|
||||
for match in re.finditer(
|
||||
r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*",
|
||||
string,
|
||||
re.S,
|
||||
):
|
||||
arg = match.group().strip()
|
||||
if arg[:1] == arg[-1:] and arg[:1] in "\"'":
|
||||
arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape")
|
||||
try:
|
||||
arg = type(string)(arg)
|
||||
except UnicodeError:
|
||||
pass
|
||||
rv.append(arg)
|
||||
return rv
|
||||
|
||||
|
||||
class Option(object):
|
||||
def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None):
|
||||
self._short_opts = []
|
||||
self._long_opts = []
|
||||
self.prefixes = set()
|
||||
|
||||
for opt in opts:
|
||||
prefix, value = split_opt(opt)
|
||||
if not prefix:
|
||||
raise ValueError("Invalid start character for option ({})".format(opt))
|
||||
self.prefixes.add(prefix[0])
|
||||
if len(prefix) == 1 and len(value) == 1:
|
||||
self._short_opts.append(opt)
|
||||
else:
|
||||
self._long_opts.append(opt)
|
||||
self.prefixes.add(prefix)
|
||||
|
||||
if action is None:
|
||||
action = "store"
|
||||
|
||||
self.dest = dest
|
||||
self.action = action
|
||||
self.nargs = nargs
|
||||
self.const = const
|
||||
self.obj = obj
|
||||
|
||||
@property
|
||||
def takes_value(self):
|
||||
return self.action in ("store", "append")
|
||||
|
||||
def process(self, value, state):
|
||||
if self.action == "store":
|
||||
state.opts[self.dest] = value
|
||||
elif self.action == "store_const":
|
||||
state.opts[self.dest] = self.const
|
||||
elif self.action == "append":
|
||||
state.opts.setdefault(self.dest, []).append(value)
|
||||
elif self.action == "append_const":
|
||||
state.opts.setdefault(self.dest, []).append(self.const)
|
||||
elif self.action == "count":
|
||||
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1
|
||||
else:
|
||||
raise ValueError("unknown action '{}'".format(self.action))
|
||||
state.order.append(self.obj)
|
||||
|
||||
|
||||
class Argument(object):
|
||||
def __init__(self, dest, nargs=1, obj=None):
|
||||
self.dest = dest
|
||||
self.nargs = nargs
|
||||
self.obj = obj
|
||||
|
||||
def process(self, value, state):
|
||||
if self.nargs > 1:
|
||||
holes = sum(1 for x in value if x is None)
|
||||
if holes == len(value):
|
||||
value = None
|
||||
elif holes != 0:
|
||||
raise BadArgumentUsage(
|
||||
"argument {} takes {} values".format(self.dest, self.nargs)
|
||||
)
|
||||
state.opts[self.dest] = value
|
||||
state.order.append(self.obj)
|
||||
|
||||
|
||||
class ParsingState(object):
|
||||
def __init__(self, rargs):
|
||||
self.opts = {}
|
||||
self.largs = []
|
||||
self.rargs = rargs
|
||||
self.order = []
|
||||
|
||||
|
||||
class OptionParser(object):
|
||||
"""The option parser is an internal class that is ultimately used to
|
||||
parse options and arguments. It's modelled after optparse and brings
|
||||
a similar but vastly simplified API. It should generally not be used
|
||||
directly as the high level Click classes wrap it for you.
|
||||
|
||||
It's not nearly as extensible as optparse or argparse as it does not
|
||||
implement features that are implemented on a higher level (such as
|
||||
types or defaults).
|
||||
|
||||
:param ctx: optionally the :class:`~click.Context` where this parser
|
||||
should go with.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx=None):
|
||||
#: The :class:`~click.Context` for this parser. This might be
|
||||
#: `None` for some advanced use cases.
|
||||
self.ctx = ctx
|
||||
#: This controls how the parser deals with interspersed arguments.
|
||||
#: If this is set to `False`, the parser will stop on the first
|
||||
#: non-option. Click uses this to implement nested subcommands
|
||||
#: safely.
|
||||
self.allow_interspersed_args = True
|
||||
#: This tells the parser how to deal with unknown options. By
|
||||
#: default it will error out (which is sensible), but there is a
|
||||
#: second mode where it will ignore it and continue processing
|
||||
#: after shifting all the unknown options into the resulting args.
|
||||
self.ignore_unknown_options = False
|
||||
if ctx is not None:
|
||||
self.allow_interspersed_args = ctx.allow_interspersed_args
|
||||
self.ignore_unknown_options = ctx.ignore_unknown_options
|
||||
self._short_opt = {}
|
||||
self._long_opt = {}
|
||||
self._opt_prefixes = {"-", "--"}
|
||||
self._args = []
|
||||
|
||||
def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None):
|
||||
"""Adds a new option named `dest` to the parser. The destination
|
||||
is not inferred (unlike with optparse) and needs to be explicitly
|
||||
provided. Action can be any of ``store``, ``store_const``,
|
||||
``append``, ``appnd_const`` or ``count``.
|
||||
|
||||
The `obj` can be used to identify the option in the order list
|
||||
that is returned from the parser.
|
||||
"""
|
||||
if obj is None:
|
||||
obj = dest
|
||||
opts = [normalize_opt(opt, self.ctx) for opt in opts]
|
||||
option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj)
|
||||
self._opt_prefixes.update(option.prefixes)
|
||||
for opt in option._short_opts:
|
||||
self._short_opt[opt] = option
|
||||
for opt in option._long_opts:
|
||||
self._long_opt[opt] = option
|
||||
|
||||
def add_argument(self, dest, nargs=1, obj=None):
|
||||
"""Adds a positional argument named `dest` to the parser.
|
||||
|
||||
The `obj` can be used to identify the option in the order list
|
||||
that is returned from the parser.
|
||||
"""
|
||||
if obj is None:
|
||||
obj = dest
|
||||
self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))
|
||||
|
||||
def parse_args(self, args):
|
||||
"""Parses positional arguments and returns ``(values, args, order)``
|
||||
for the parsed options and arguments as well as the leftover
|
||||
arguments if there are any. The order is a list of objects as they
|
||||
appear on the command line. If arguments appear multiple times they
|
||||
will be memorized multiple times as well.
|
||||
"""
|
||||
state = ParsingState(args)
|
||||
try:
|
||||
self._process_args_for_options(state)
|
||||
self._process_args_for_args(state)
|
||||
except UsageError:
|
||||
if self.ctx is None or not self.ctx.resilient_parsing:
|
||||
raise
|
||||
return state.opts, state.largs, state.order
|
||||
|
||||
def _process_args_for_args(self, state):
|
||||
pargs, args = _unpack_args(
|
||||
state.largs + state.rargs, [x.nargs for x in self._args]
|
||||
)
|
||||
|
||||
for idx, arg in enumerate(self._args):
|
||||
arg.process(pargs[idx], state)
|
||||
|
||||
state.largs = args
|
||||
state.rargs = []
|
||||
|
||||
def _process_args_for_options(self, state):
|
||||
while state.rargs:
|
||||
arg = state.rargs.pop(0)
|
||||
arglen = len(arg)
|
||||
# Double dashes always handled explicitly regardless of what
|
||||
# prefixes are valid.
|
||||
if arg == "--":
|
||||
return
|
||||
elif arg[:1] in self._opt_prefixes and arglen > 1:
|
||||
self._process_opts(arg, state)
|
||||
elif self.allow_interspersed_args:
|
||||
state.largs.append(arg)
|
||||
else:
|
||||
state.rargs.insert(0, arg)
|
||||
return
|
||||
|
||||
# Say this is the original argument list:
|
||||
# [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
|
||||
# ^
|
||||
# (we are about to process arg(i)).
|
||||
#
|
||||
# Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
|
||||
# [arg0, ..., arg(i-1)] (any options and their arguments will have
|
||||
# been removed from largs).
|
||||
#
|
||||
# The while loop will usually consume 1 or more arguments per pass.
|
||||
# If it consumes 1 (eg. arg is an option that takes no arguments),
|
||||
# then after _process_arg() is done the situation is:
|
||||
#
|
||||
# largs = subset of [arg0, ..., arg(i)]
|
||||
# rargs = [arg(i+1), ..., arg(N-1)]
|
||||
#
|
||||
# If allow_interspersed_args is false, largs will always be
|
||||
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
||||
# not a very interesting subset!
|
||||
|
||||
def _match_long_opt(self, opt, explicit_value, state):
|
||||
if opt not in self._long_opt:
|
||||
possibilities = [word for word in self._long_opt if word.startswith(opt)]
|
||||
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
|
||||
|
||||
option = self._long_opt[opt]
|
||||
if option.takes_value:
|
||||
# At this point it's safe to modify rargs by injecting the
|
||||
# explicit value, because no exception is raised in this
|
||||
# branch. This means that the inserted value will be fully
|
||||
# consumed.
|
||||
if explicit_value is not None:
|
||||
state.rargs.insert(0, explicit_value)
|
||||
|
||||
nargs = option.nargs
|
||||
if len(state.rargs) < nargs:
|
||||
_error_opt_args(nargs, opt)
|
||||
elif nargs == 1:
|
||||
value = state.rargs.pop(0)
|
||||
else:
|
||||
value = tuple(state.rargs[:nargs])
|
||||
del state.rargs[:nargs]
|
||||
|
||||
elif explicit_value is not None:
|
||||
raise BadOptionUsage(opt, "{} option does not take a value".format(opt))
|
||||
|
||||
else:
|
||||
value = None
|
||||
|
||||
option.process(value, state)
|
||||
|
||||
def _match_short_opt(self, arg, state):
|
||||
stop = False
|
||||
i = 1
|
||||
prefix = arg[0]
|
||||
unknown_options = []
|
||||
|
||||
for ch in arg[1:]:
|
||||
opt = normalize_opt(prefix + ch, self.ctx)
|
||||
option = self._short_opt.get(opt)
|
||||
i += 1
|
||||
|
||||
if not option:
|
||||
if self.ignore_unknown_options:
|
||||
unknown_options.append(ch)
|
||||
continue
|
||||
raise NoSuchOption(opt, ctx=self.ctx)
|
||||
if option.takes_value:
|
||||
# Any characters left in arg? Pretend they're the
|
||||
# next arg, and stop consuming characters of arg.
|
||||
if i < len(arg):
|
||||
state.rargs.insert(0, arg[i:])
|
||||
stop = True
|
||||
|
||||
nargs = option.nargs
|
||||
if len(state.rargs) < nargs:
|
||||
_error_opt_args(nargs, opt)
|
||||
elif nargs == 1:
|
||||
value = state.rargs.pop(0)
|
||||
else:
|
||||
value = tuple(state.rargs[:nargs])
|
||||
del state.rargs[:nargs]
|
||||
|
||||
else:
|
||||
value = None
|
||||
|
||||
option.process(value, state)
|
||||
|
||||
if stop:
|
||||
break
|
||||
|
||||
# If we got any unknown options we re-combinate the string of the
|
||||
# remaining options and re-attach the prefix, then report that
|
||||
# to the state as new larg. This way there is basic combinatorics
|
||||
# that can be achieved while still ignoring unknown arguments.
|
||||
if self.ignore_unknown_options and unknown_options:
|
||||
state.largs.append("{}{}".format(prefix, "".join(unknown_options)))
|
||||
|
||||
def _process_opts(self, arg, state):
|
||||
explicit_value = None
|
||||
# Long option handling happens in two parts. The first part is
|
||||
# supporting explicitly attached values. In any case, we will try
|
||||
# to long match the option first.
|
||||
if "=" in arg:
|
||||
long_opt, explicit_value = arg.split("=", 1)
|
||||
else:
|
||||
long_opt = arg
|
||||
norm_long_opt = normalize_opt(long_opt, self.ctx)
|
||||
|
||||
# At this point we will match the (assumed) long option through
|
||||
# the long option matching code. Note that this allows options
|
||||
# like "-foo" to be matched as long options.
|
||||
try:
|
||||
self._match_long_opt(norm_long_opt, explicit_value, state)
|
||||
except NoSuchOption:
|
||||
# At this point the long option matching failed, and we need
|
||||
# to try with short options. However there is a special rule
|
||||
# which says, that if we have a two character options prefix
|
||||
# (applies to "--foo" for instance), we do not dispatch to the
|
||||
# short option code and will instead raise the no option
|
||||
# error.
|
||||
if arg[:2] not in self._opt_prefixes:
|
||||
return self._match_short_opt(arg, state)
|
||||
if not self.ignore_unknown_options:
|
||||
raise
|
||||
state.largs.append(arg)
|
||||
@@ -1,681 +0,0 @@
|
||||
import inspect
|
||||
import io
|
||||
import itertools
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from ._compat import DEFAULT_COLUMNS
|
||||
from ._compat import get_winterm_size
|
||||
from ._compat import isatty
|
||||
from ._compat import raw_input
|
||||
from ._compat import string_types
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import text_type
|
||||
from ._compat import WIN
|
||||
from .exceptions import Abort
|
||||
from .exceptions import UsageError
|
||||
from .globals import resolve_color_default
|
||||
from .types import Choice
|
||||
from .types import convert_type
|
||||
from .types import Path
|
||||
from .utils import echo
|
||||
from .utils import LazyFile
|
||||
|
||||
# The prompt functions to use. The doc tools currently override these
|
||||
# functions to customize how they work.
|
||||
visible_prompt_func = raw_input
|
||||
|
||||
_ansi_colors = {
|
||||
"black": 30,
|
||||
"red": 31,
|
||||
"green": 32,
|
||||
"yellow": 33,
|
||||
"blue": 34,
|
||||
"magenta": 35,
|
||||
"cyan": 36,
|
||||
"white": 37,
|
||||
"reset": 39,
|
||||
"bright_black": 90,
|
||||
"bright_red": 91,
|
||||
"bright_green": 92,
|
||||
"bright_yellow": 93,
|
||||
"bright_blue": 94,
|
||||
"bright_magenta": 95,
|
||||
"bright_cyan": 96,
|
||||
"bright_white": 97,
|
||||
}
|
||||
_ansi_reset_all = "\033[0m"
|
||||
|
||||
|
||||
def hidden_prompt_func(prompt):
|
||||
import getpass
|
||||
|
||||
return getpass.getpass(prompt)
|
||||
|
||||
|
||||
def _build_prompt(
|
||||
text, suffix, show_default=False, default=None, show_choices=True, type=None
|
||||
):
|
||||
prompt = text
|
||||
if type is not None and show_choices and isinstance(type, Choice):
|
||||
prompt += " ({})".format(", ".join(map(str, type.choices)))
|
||||
if default is not None and show_default:
|
||||
prompt = "{} [{}]".format(prompt, _format_default(default))
|
||||
return prompt + suffix
|
||||
|
||||
|
||||
def _format_default(default):
|
||||
if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
|
||||
return default.name
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def prompt(
|
||||
text,
|
||||
default=None,
|
||||
hide_input=False,
|
||||
confirmation_prompt=False,
|
||||
type=None,
|
||||
value_proc=None,
|
||||
prompt_suffix=": ",
|
||||
show_default=True,
|
||||
err=False,
|
||||
show_choices=True,
|
||||
):
|
||||
"""Prompts a user for input. This is a convenience function that can
|
||||
be used to prompt a user for input later.
|
||||
|
||||
If the user aborts the input by sending a interrupt signal, this
|
||||
function will catch it and raise a :exc:`Abort` exception.
|
||||
|
||||
.. versionadded:: 7.0
|
||||
Added the show_choices parameter.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
Added unicode support for cmd.exe on Windows.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
Added the `err` parameter.
|
||||
|
||||
:param text: the text to show for the prompt.
|
||||
:param default: the default value to use if no input happens. If this
|
||||
is not given it will prompt until it's aborted.
|
||||
:param hide_input: if this is set to true then the input value will
|
||||
be hidden.
|
||||
:param confirmation_prompt: asks for confirmation for the value.
|
||||
:param type: the type to use to check the value against.
|
||||
:param value_proc: if this parameter is provided it's a function that
|
||||
is invoked instead of the type conversion to
|
||||
convert a value.
|
||||
:param prompt_suffix: a suffix that should be added to the prompt.
|
||||
:param show_default: shows or hides the default value in the prompt.
|
||||
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||
``stdout``, the same as with echo.
|
||||
:param show_choices: Show or hide choices if the passed type is a Choice.
|
||||
For example if type is a Choice of either day or week,
|
||||
show_choices is true and text is "Group by" then the
|
||||
prompt will be "Group by (day, week): ".
|
||||
"""
|
||||
result = None
|
||||
|
||||
def prompt_func(text):
|
||||
f = hidden_prompt_func if hide_input else visible_prompt_func
|
||||
try:
|
||||
# Write the prompt separately so that we get nice
|
||||
# coloring through colorama on Windows
|
||||
echo(text, nl=False, err=err)
|
||||
return f("")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
# getpass doesn't print a newline if the user aborts input with ^C.
|
||||
# Allegedly this behavior is inherited from getpass(3).
|
||||
# A doc bug has been filed at https://bugs.python.org/issue24711
|
||||
if hide_input:
|
||||
echo(None, err=err)
|
||||
raise Abort()
|
||||
|
||||
if value_proc is None:
|
||||
value_proc = convert_type(type, default)
|
||||
|
||||
prompt = _build_prompt(
|
||||
text, prompt_suffix, show_default, default, show_choices, type
|
||||
)
|
||||
|
||||
while 1:
|
||||
while 1:
|
||||
value = prompt_func(prompt)
|
||||
if value:
|
||||
break
|
||||
elif default is not None:
|
||||
if isinstance(value_proc, Path):
|
||||
# validate Path default value(exists, dir_okay etc.)
|
||||
value = default
|
||||
break
|
||||
return default
|
||||
try:
|
||||
result = value_proc(value)
|
||||
except UsageError as e:
|
||||
echo("Error: {}".format(e.message), err=err) # noqa: B306
|
||||
continue
|
||||
if not confirmation_prompt:
|
||||
return result
|
||||
while 1:
|
||||
value2 = prompt_func("Repeat for confirmation: ")
|
||||
if value2:
|
||||
break
|
||||
if value == value2:
|
||||
return result
|
||||
echo("Error: the two entered values do not match", err=err)
|
||||
|
||||
|
||||
def confirm(
|
||||
text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False
|
||||
):
|
||||
"""Prompts for confirmation (yes/no question).
|
||||
|
||||
If the user aborts the input by sending a interrupt signal this
|
||||
function will catch it and raise a :exc:`Abort` exception.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
Added the `err` parameter.
|
||||
|
||||
:param text: the question to ask.
|
||||
:param default: the default for the prompt.
|
||||
:param abort: if this is set to `True` a negative answer aborts the
|
||||
exception by raising :exc:`Abort`.
|
||||
:param prompt_suffix: a suffix that should be added to the prompt.
|
||||
:param show_default: shows or hides the default value in the prompt.
|
||||
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||
``stdout``, the same as with echo.
|
||||
"""
|
||||
prompt = _build_prompt(
|
||||
text, prompt_suffix, show_default, "Y/n" if default else "y/N"
|
||||
)
|
||||
while 1:
|
||||
try:
|
||||
# Write the prompt separately so that we get nice
|
||||
# coloring through colorama on Windows
|
||||
echo(prompt, nl=False, err=err)
|
||||
value = visible_prompt_func("").lower().strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
raise Abort()
|
||||
if value in ("y", "yes"):
|
||||
rv = True
|
||||
elif value in ("n", "no"):
|
||||
rv = False
|
||||
elif value == "":
|
||||
rv = default
|
||||
else:
|
||||
echo("Error: invalid input", err=err)
|
||||
continue
|
||||
break
|
||||
if abort and not rv:
|
||||
raise Abort()
|
||||
return rv
|
||||
|
||||
|
||||
def get_terminal_size():
|
||||
"""Returns the current size of the terminal as tuple in the form
|
||||
``(width, height)`` in columns and rows.
|
||||
"""
|
||||
# If shutil has get_terminal_size() (Python 3.3 and later) use that
|
||||
if sys.version_info >= (3, 3):
|
||||
import shutil
|
||||
|
||||
shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None)
|
||||
if shutil_get_terminal_size:
|
||||
sz = shutil_get_terminal_size()
|
||||
return sz.columns, sz.lines
|
||||
|
||||
# We provide a sensible default for get_winterm_size() when being invoked
|
||||
# inside a subprocess. Without this, it would not provide a useful input.
|
||||
if get_winterm_size is not None:
|
||||
size = get_winterm_size()
|
||||
if size == (0, 0):
|
||||
return (79, 24)
|
||||
else:
|
||||
return size
|
||||
|
||||
def ioctl_gwinsz(fd):
|
||||
try:
|
||||
import fcntl
|
||||
import termios
|
||||
|
||||
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
|
||||
except Exception:
|
||||
return
|
||||
return cr
|
||||
|
||||
cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)
|
||||
if not cr:
|
||||
try:
|
||||
fd = os.open(os.ctermid(), os.O_RDONLY)
|
||||
try:
|
||||
cr = ioctl_gwinsz(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
if not cr or not cr[0] or not cr[1]:
|
||||
cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS))
|
||||
return int(cr[1]), int(cr[0])
|
||||
|
||||
|
||||
def echo_via_pager(text_or_generator, color=None):
|
||||
"""This function takes a text and shows it via an environment specific
|
||||
pager on stdout.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Added the `color` flag.
|
||||
|
||||
:param text_or_generator: the text to page, or alternatively, a
|
||||
generator emitting the text to page.
|
||||
:param color: controls if the pager supports ANSI colors or not. The
|
||||
default is autodetection.
|
||||
"""
|
||||
color = resolve_color_default(color)
|
||||
|
||||
if inspect.isgeneratorfunction(text_or_generator):
|
||||
i = text_or_generator()
|
||||
elif isinstance(text_or_generator, string_types):
|
||||
i = [text_or_generator]
|
||||
else:
|
||||
i = iter(text_or_generator)
|
||||
|
||||
# convert every element of i to a text type if necessary
|
||||
text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i)
|
||||
|
||||
from ._termui_impl import pager
|
||||
|
||||
return pager(itertools.chain(text_generator, "\n"), color)
|
||||
|
||||
|
||||
def progressbar(
|
||||
iterable=None,
|
||||
length=None,
|
||||
label=None,
|
||||
show_eta=True,
|
||||
show_percent=None,
|
||||
show_pos=False,
|
||||
item_show_func=None,
|
||||
fill_char="#",
|
||||
empty_char="-",
|
||||
bar_template="%(label)s [%(bar)s] %(info)s",
|
||||
info_sep=" ",
|
||||
width=36,
|
||||
file=None,
|
||||
color=None,
|
||||
):
|
||||
"""This function creates an iterable context manager that can be used
|
||||
to iterate over something while showing a progress bar. It will
|
||||
either iterate over the `iterable` or `length` items (that are counted
|
||||
up). While iteration happens, this function will print a rendered
|
||||
progress bar to the given `file` (defaults to stdout) and will attempt
|
||||
to calculate remaining time and more. By default, this progress bar
|
||||
will not be rendered if the file is not a terminal.
|
||||
|
||||
The context manager creates the progress bar. When the context
|
||||
manager is entered the progress bar is already created. With every
|
||||
iteration over the progress bar, the iterable passed to the bar is
|
||||
advanced and the bar is updated. When the context manager exits,
|
||||
a newline is printed and the progress bar is finalized on screen.
|
||||
|
||||
Note: The progress bar is currently designed for use cases where the
|
||||
total progress can be expected to take at least several seconds.
|
||||
Because of this, the ProgressBar class object won't display
|
||||
progress that is considered too fast, and progress where the time
|
||||
between steps is less than a second.
|
||||
|
||||
No printing must happen or the progress bar will be unintentionally
|
||||
destroyed.
|
||||
|
||||
Example usage::
|
||||
|
||||
with progressbar(items) as bar:
|
||||
for item in bar:
|
||||
do_something_with(item)
|
||||
|
||||
Alternatively, if no iterable is specified, one can manually update the
|
||||
progress bar through the `update()` method instead of directly
|
||||
iterating over the progress bar. The update method accepts the number
|
||||
of steps to increment the bar with::
|
||||
|
||||
with progressbar(length=chunks.total_bytes) as bar:
|
||||
for chunk in chunks:
|
||||
process_chunk(chunk)
|
||||
bar.update(chunks.bytes)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. versionadded:: 4.0
|
||||
Added the `color` parameter. Added a `update` method to the
|
||||
progressbar object.
|
||||
|
||||
:param iterable: an iterable to iterate over. If not provided the length
|
||||
is required.
|
||||
:param length: the number of items to iterate over. By default the
|
||||
progressbar will attempt to ask the iterator about its
|
||||
length, which might or might not work. If an iterable is
|
||||
also provided this parameter can be used to override the
|
||||
length. If an iterable is not provided the progress bar
|
||||
will iterate over a range of that length.
|
||||
:param label: the label to show next to the progress bar.
|
||||
:param show_eta: enables or disables the estimated time display. This is
|
||||
automatically disabled if the length cannot be
|
||||
determined.
|
||||
:param show_percent: enables or disables the percentage display. The
|
||||
default is `True` if the iterable has a length or
|
||||
`False` if not.
|
||||
:param show_pos: enables or disables the absolute position display. The
|
||||
default is `False`.
|
||||
:param item_show_func: a function called with the current item which
|
||||
can return a string to show the current item
|
||||
next to the progress bar. Note that the current
|
||||
item can be `None`!
|
||||
:param fill_char: the character to use to show the filled part of the
|
||||
progress bar.
|
||||
:param empty_char: the character to use to show the non-filled part of
|
||||
the progress bar.
|
||||
:param bar_template: the format string to use as template for the bar.
|
||||
The parameters in it are ``label`` for the label,
|
||||
``bar`` for the progress bar and ``info`` for the
|
||||
info section.
|
||||
:param info_sep: the separator between multiple info items (eta etc.)
|
||||
:param width: the width of the progress bar in characters, 0 means full
|
||||
terminal width
|
||||
:param file: the file to write to. If this is not a terminal then
|
||||
only the label is printed.
|
||||
:param color: controls if the terminal supports ANSI colors or not. The
|
||||
default is autodetection. This is only needed if ANSI
|
||||
codes are included anywhere in the progress bar output
|
||||
which is not the case by default.
|
||||
"""
|
||||
from ._termui_impl import ProgressBar
|
||||
|
||||
color = resolve_color_default(color)
|
||||
return ProgressBar(
|
||||
iterable=iterable,
|
||||
length=length,
|
||||
show_eta=show_eta,
|
||||
show_percent=show_percent,
|
||||
show_pos=show_pos,
|
||||
item_show_func=item_show_func,
|
||||
fill_char=fill_char,
|
||||
empty_char=empty_char,
|
||||
bar_template=bar_template,
|
||||
info_sep=info_sep,
|
||||
file=file,
|
||||
label=label,
|
||||
width=width,
|
||||
color=color,
|
||||
)
|
||||
|
||||
|
||||
def clear():
|
||||
"""Clears the terminal screen. This will have the effect of clearing
|
||||
the whole visible space of the terminal and moving the cursor to the
|
||||
top left. This does not do anything if not connected to a terminal.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if not isatty(sys.stdout):
|
||||
return
|
||||
# If we're on Windows and we don't have colorama available, then we
|
||||
# clear the screen by shelling out. Otherwise we can use an escape
|
||||
# sequence.
|
||||
if WIN:
|
||||
os.system("cls")
|
||||
else:
|
||||
sys.stdout.write("\033[2J\033[1;1H")
|
||||
|
||||
|
||||
def style(
|
||||
text,
|
||||
fg=None,
|
||||
bg=None,
|
||||
bold=None,
|
||||
dim=None,
|
||||
underline=None,
|
||||
blink=None,
|
||||
reverse=None,
|
||||
reset=True,
|
||||
):
|
||||
"""Styles a text with ANSI styles and returns the new string. By
|
||||
default the styling is self contained which means that at the end
|
||||
of the string a reset code is issued. This can be prevented by
|
||||
passing ``reset=False``.
|
||||
|
||||
Examples::
|
||||
|
||||
click.echo(click.style('Hello World!', fg='green'))
|
||||
click.echo(click.style('ATTENTION!', blink=True))
|
||||
click.echo(click.style('Some things', reverse=True, fg='cyan'))
|
||||
|
||||
Supported color names:
|
||||
|
||||
* ``black`` (might be a gray)
|
||||
* ``red``
|
||||
* ``green``
|
||||
* ``yellow`` (might be an orange)
|
||||
* ``blue``
|
||||
* ``magenta``
|
||||
* ``cyan``
|
||||
* ``white`` (might be light gray)
|
||||
* ``bright_black``
|
||||
* ``bright_red``
|
||||
* ``bright_green``
|
||||
* ``bright_yellow``
|
||||
* ``bright_blue``
|
||||
* ``bright_magenta``
|
||||
* ``bright_cyan``
|
||||
* ``bright_white``
|
||||
* ``reset`` (reset the color code only)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. versionadded:: 7.0
|
||||
Added support for bright colors.
|
||||
|
||||
:param text: the string to style with ansi codes.
|
||||
:param fg: if provided this will become the foreground color.
|
||||
:param bg: if provided this will become the background color.
|
||||
:param bold: if provided this will enable or disable bold mode.
|
||||
:param dim: if provided this will enable or disable dim mode. This is
|
||||
badly supported.
|
||||
:param underline: if provided this will enable or disable underline.
|
||||
:param blink: if provided this will enable or disable blinking.
|
||||
:param reverse: if provided this will enable or disable inverse
|
||||
rendering (foreground becomes background and the
|
||||
other way round).
|
||||
:param reset: by default a reset-all code is added at the end of the
|
||||
string which means that styles do not carry over. This
|
||||
can be disabled to compose styles.
|
||||
"""
|
||||
bits = []
|
||||
if fg:
|
||||
try:
|
||||
bits.append("\033[{}m".format(_ansi_colors[fg]))
|
||||
except KeyError:
|
||||
raise TypeError("Unknown color '{}'".format(fg))
|
||||
if bg:
|
||||
try:
|
||||
bits.append("\033[{}m".format(_ansi_colors[bg] + 10))
|
||||
except KeyError:
|
||||
raise TypeError("Unknown color '{}'".format(bg))
|
||||
if bold is not None:
|
||||
bits.append("\033[{}m".format(1 if bold else 22))
|
||||
if dim is not None:
|
||||
bits.append("\033[{}m".format(2 if dim else 22))
|
||||
if underline is not None:
|
||||
bits.append("\033[{}m".format(4 if underline else 24))
|
||||
if blink is not None:
|
||||
bits.append("\033[{}m".format(5 if blink else 25))
|
||||
if reverse is not None:
|
||||
bits.append("\033[{}m".format(7 if reverse else 27))
|
||||
bits.append(text)
|
||||
if reset:
|
||||
bits.append(_ansi_reset_all)
|
||||
return "".join(bits)
|
||||
|
||||
|
||||
def unstyle(text):
|
||||
"""Removes ANSI styling information from a string. Usually it's not
|
||||
necessary to use this function as Click's echo function will
|
||||
automatically remove styling if necessary.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
:param text: the text to remove style information from.
|
||||
"""
|
||||
return strip_ansi(text)
|
||||
|
||||
|
||||
def secho(message=None, file=None, nl=True, err=False, color=None, **styles):
|
||||
"""This function combines :func:`echo` and :func:`style` into one
|
||||
call. As such the following two calls are the same::
|
||||
|
||||
click.secho('Hello World!', fg='green')
|
||||
click.echo(click.style('Hello World!', fg='green'))
|
||||
|
||||
All keyword arguments are forwarded to the underlying functions
|
||||
depending on which one they go with.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if message is not None:
|
||||
message = style(message, **styles)
|
||||
return echo(message, file=file, nl=nl, err=err, color=color)
|
||||
|
||||
|
||||
def edit(
|
||||
text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None
|
||||
):
|
||||
r"""Edits the given text in the defined editor. If an editor is given
|
||||
(should be the full path to the executable but the regular operating
|
||||
system search path is used for finding the executable) it overrides
|
||||
the detected editor. Optionally, some environment variables can be
|
||||
used. If the editor is closed without changes, `None` is returned. In
|
||||
case a file is edited directly the return value is always `None` and
|
||||
`require_save` and `extension` are ignored.
|
||||
|
||||
If the editor cannot be opened a :exc:`UsageError` is raised.
|
||||
|
||||
Note for Windows: to simplify cross-platform usage, the newlines are
|
||||
automatically converted from POSIX to Windows and vice versa. As such,
|
||||
the message here will have ``\n`` as newline markers.
|
||||
|
||||
:param text: the text to edit.
|
||||
:param editor: optionally the editor to use. Defaults to automatic
|
||||
detection.
|
||||
:param env: environment variables to forward to the editor.
|
||||
:param require_save: if this is true, then not saving in the editor
|
||||
will make the return value become `None`.
|
||||
:param extension: the extension to tell the editor about. This defaults
|
||||
to `.txt` but changing this might change syntax
|
||||
highlighting.
|
||||
:param filename: if provided it will edit this file instead of the
|
||||
provided text contents. It will not use a temporary
|
||||
file as an indirection in that case.
|
||||
"""
|
||||
from ._termui_impl import Editor
|
||||
|
||||
editor = Editor(
|
||||
editor=editor, env=env, require_save=require_save, extension=extension
|
||||
)
|
||||
if filename is None:
|
||||
return editor.edit(text)
|
||||
editor.edit_file(filename)
|
||||
|
||||
|
||||
def launch(url, wait=False, locate=False):
|
||||
"""This function launches the given URL (or filename) in the default
|
||||
viewer application for this file type. If this is an executable, it
|
||||
might launch the executable in a new session. The return value is
|
||||
the exit code of the launched application. Usually, ``0`` indicates
|
||||
success.
|
||||
|
||||
Examples::
|
||||
|
||||
click.launch('https://click.palletsprojects.com/')
|
||||
click.launch('/my/downloaded/file', locate=True)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
:param url: URL or filename of the thing to launch.
|
||||
:param wait: waits for the program to stop.
|
||||
:param locate: if this is set to `True` then instead of launching the
|
||||
application associated with the URL it will attempt to
|
||||
launch a file manager with the file located. This
|
||||
might have weird effects if the URL does not point to
|
||||
the filesystem.
|
||||
"""
|
||||
from ._termui_impl import open_url
|
||||
|
||||
return open_url(url, wait=wait, locate=locate)
|
||||
|
||||
|
||||
# If this is provided, getchar() calls into this instead. This is used
|
||||
# for unittesting purposes.
|
||||
_getchar = None
|
||||
|
||||
|
||||
def getchar(echo=False):
|
||||
"""Fetches a single character from the terminal and returns it. This
|
||||
will always return a unicode character and under certain rare
|
||||
circumstances this might return more than one character. The
|
||||
situations which more than one character is returned is when for
|
||||
whatever reason multiple characters end up in the terminal buffer or
|
||||
standard input was not actually a terminal.
|
||||
|
||||
Note that this will always read from the terminal, even if something
|
||||
is piped into the standard input.
|
||||
|
||||
Note for Windows: in rare cases when typing non-ASCII characters, this
|
||||
function might wait for a second character and then return both at once.
|
||||
This is because certain Unicode characters look like special-key markers.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
:param echo: if set to `True`, the character read will also show up on
|
||||
the terminal. The default is to not show it.
|
||||
"""
|
||||
f = _getchar
|
||||
if f is None:
|
||||
from ._termui_impl import getchar as f
|
||||
return f(echo)
|
||||
|
||||
|
||||
def raw_terminal():
|
||||
from ._termui_impl import raw_terminal as f
|
||||
|
||||
return f()
|
||||
|
||||
|
||||
def pause(info="Press any key to continue ...", err=False):
|
||||
"""This command stops execution and waits for the user to press any
|
||||
key to continue. This is similar to the Windows batch "pause"
|
||||
command. If the program is not run through a terminal, this command
|
||||
will instead do nothing.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. versionadded:: 4.0
|
||||
Added the `err` parameter.
|
||||
|
||||
:param info: the info string to print before pausing.
|
||||
:param err: if set to message goes to ``stderr`` instead of
|
||||
``stdout``, the same as with echo.
|
||||
"""
|
||||
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
||||
return
|
||||
try:
|
||||
if info:
|
||||
echo(info, nl=False, err=err)
|
||||
try:
|
||||
getchar()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
finally:
|
||||
if info:
|
||||
echo(err=err)
|
||||
@@ -1,382 +0,0 @@
|
||||
import contextlib
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from . import formatting
|
||||
from . import termui
|
||||
from . import utils
|
||||
from ._compat import iteritems
|
||||
from ._compat import PY2
|
||||
from ._compat import string_types
|
||||
|
||||
|
||||
if PY2:
|
||||
from cStringIO import StringIO
|
||||
else:
|
||||
import io
|
||||
from ._compat import _find_binary_reader
|
||||
|
||||
|
||||
class EchoingStdin(object):
|
||||
def __init__(self, input, output):
|
||||
self._input = input
|
||||
self._output = output
|
||||
|
||||
def __getattr__(self, x):
|
||||
return getattr(self._input, x)
|
||||
|
||||
def _echo(self, rv):
|
||||
self._output.write(rv)
|
||||
return rv
|
||||
|
||||
def read(self, n=-1):
|
||||
return self._echo(self._input.read(n))
|
||||
|
||||
def readline(self, n=-1):
|
||||
return self._echo(self._input.readline(n))
|
||||
|
||||
def readlines(self):
|
||||
return [self._echo(x) for x in self._input.readlines()]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._echo(x) for x in self._input)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._input)
|
||||
|
||||
|
||||
def make_input_stream(input, charset):
|
||||
# Is already an input stream.
|
||||
if hasattr(input, "read"):
|
||||
if PY2:
|
||||
return input
|
||||
rv = _find_binary_reader(input)
|
||||
if rv is not None:
|
||||
return rv
|
||||
raise TypeError("Could not find binary reader for input stream.")
|
||||
|
||||
if input is None:
|
||||
input = b""
|
||||
elif not isinstance(input, bytes):
|
||||
input = input.encode(charset)
|
||||
if PY2:
|
||||
return StringIO(input)
|
||||
return io.BytesIO(input)
|
||||
|
||||
|
||||
class Result(object):
|
||||
"""Holds the captured result of an invoked CLI script."""
|
||||
|
||||
def __init__(
|
||||
self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None
|
||||
):
|
||||
#: The runner that created the result
|
||||
self.runner = runner
|
||||
#: The standard output as bytes.
|
||||
self.stdout_bytes = stdout_bytes
|
||||
#: The standard error as bytes, or None if not available
|
||||
self.stderr_bytes = stderr_bytes
|
||||
#: The exit code as integer.
|
||||
self.exit_code = exit_code
|
||||
#: The exception that happened if one did.
|
||||
self.exception = exception
|
||||
#: The traceback
|
||||
self.exc_info = exc_info
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
"""The (standard) output as unicode string."""
|
||||
return self.stdout
|
||||
|
||||
@property
|
||||
def stdout(self):
|
||||
"""The standard output as unicode string."""
|
||||
return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
|
||||
"\r\n", "\n"
|
||||
)
|
||||
|
||||
@property
|
||||
def stderr(self):
|
||||
"""The standard error as unicode string."""
|
||||
if self.stderr_bytes is None:
|
||||
raise ValueError("stderr not separately captured")
|
||||
return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
|
||||
"\r\n", "\n"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<{} {}>".format(
|
||||
type(self).__name__, repr(self.exception) if self.exception else "okay"
|
||||
)
|
||||
|
||||
|
||||
class CliRunner(object):
|
||||
"""The CLI runner provides functionality to invoke a Click command line
|
||||
script for unittesting purposes in a isolated environment. This only
|
||||
works in single-threaded systems without any concurrency as it changes the
|
||||
global interpreter state.
|
||||
|
||||
:param charset: the character set for the input and output data. This is
|
||||
UTF-8 by default and should not be changed currently as
|
||||
the reporting to Click only works in Python 2 properly.
|
||||
:param env: a dictionary with environment variables for overriding.
|
||||
:param echo_stdin: if this is set to `True`, then reading from stdin writes
|
||||
to stdout. This is useful for showing examples in
|
||||
some circumstances. Note that regular prompts
|
||||
will automatically echo the input.
|
||||
:param mix_stderr: if this is set to `False`, then stdout and stderr are
|
||||
preserved as independent streams. This is useful for
|
||||
Unix-philosophy apps that have predictable stdout and
|
||||
noisy stderr, such that each may be measured
|
||||
independently
|
||||
"""
|
||||
|
||||
def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True):
|
||||
if charset is None:
|
||||
charset = "utf-8"
|
||||
self.charset = charset
|
||||
self.env = env or {}
|
||||
self.echo_stdin = echo_stdin
|
||||
self.mix_stderr = mix_stderr
|
||||
|
||||
def get_default_prog_name(self, cli):
|
||||
"""Given a command object it will return the default program name
|
||||
for it. The default is the `name` attribute or ``"root"`` if not
|
||||
set.
|
||||
"""
|
||||
return cli.name or "root"
|
||||
|
||||
def make_env(self, overrides=None):
|
||||
"""Returns the environment overrides for invoking a script."""
|
||||
rv = dict(self.env)
|
||||
if overrides:
|
||||
rv.update(overrides)
|
||||
return rv
|
||||
|
||||
@contextlib.contextmanager
|
||||
def isolation(self, input=None, env=None, color=False):
|
||||
"""A context manager that sets up the isolation for invoking of a
|
||||
command line tool. This sets up stdin with the given input data
|
||||
and `os.environ` with the overrides from the given dictionary.
|
||||
This also rebinds some internals in Click to be mocked (like the
|
||||
prompt functionality).
|
||||
|
||||
This is automatically done in the :meth:`invoke` method.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
The ``color`` parameter was added.
|
||||
|
||||
:param input: the input stream to put into sys.stdin.
|
||||
:param env: the environment overrides as dictionary.
|
||||
:param color: whether the output should contain color codes. The
|
||||
application can still override this explicitly.
|
||||
"""
|
||||
input = make_input_stream(input, self.charset)
|
||||
|
||||
old_stdin = sys.stdin
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
old_forced_width = formatting.FORCED_WIDTH
|
||||
formatting.FORCED_WIDTH = 80
|
||||
|
||||
env = self.make_env(env)
|
||||
|
||||
if PY2:
|
||||
bytes_output = StringIO()
|
||||
if self.echo_stdin:
|
||||
input = EchoingStdin(input, bytes_output)
|
||||
sys.stdout = bytes_output
|
||||
if not self.mix_stderr:
|
||||
bytes_error = StringIO()
|
||||
sys.stderr = bytes_error
|
||||
else:
|
||||
bytes_output = io.BytesIO()
|
||||
if self.echo_stdin:
|
||||
input = EchoingStdin(input, bytes_output)
|
||||
input = io.TextIOWrapper(input, encoding=self.charset)
|
||||
sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset)
|
||||
if not self.mix_stderr:
|
||||
bytes_error = io.BytesIO()
|
||||
sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset)
|
||||
|
||||
if self.mix_stderr:
|
||||
sys.stderr = sys.stdout
|
||||
|
||||
sys.stdin = input
|
||||
|
||||
def visible_input(prompt=None):
|
||||
sys.stdout.write(prompt or "")
|
||||
val = input.readline().rstrip("\r\n")
|
||||
sys.stdout.write("{}\n".format(val))
|
||||
sys.stdout.flush()
|
||||
return val
|
||||
|
||||
def hidden_input(prompt=None):
|
||||
sys.stdout.write("{}\n".format(prompt or ""))
|
||||
sys.stdout.flush()
|
||||
return input.readline().rstrip("\r\n")
|
||||
|
||||
def _getchar(echo):
|
||||
char = sys.stdin.read(1)
|
||||
if echo:
|
||||
sys.stdout.write(char)
|
||||
sys.stdout.flush()
|
||||
return char
|
||||
|
||||
default_color = color
|
||||
|
||||
def should_strip_ansi(stream=None, color=None):
|
||||
if color is None:
|
||||
return not default_color
|
||||
return not color
|
||||
|
||||
old_visible_prompt_func = termui.visible_prompt_func
|
||||
old_hidden_prompt_func = termui.hidden_prompt_func
|
||||
old__getchar_func = termui._getchar
|
||||
old_should_strip_ansi = utils.should_strip_ansi
|
||||
termui.visible_prompt_func = visible_input
|
||||
termui.hidden_prompt_func = hidden_input
|
||||
termui._getchar = _getchar
|
||||
utils.should_strip_ansi = should_strip_ansi
|
||||
|
||||
old_env = {}
|
||||
try:
|
||||
for key, value in iteritems(env):
|
||||
old_env[key] = os.environ.get(key)
|
||||
if value is None:
|
||||
try:
|
||||
del os.environ[key]
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
os.environ[key] = value
|
||||
yield (bytes_output, not self.mix_stderr and bytes_error)
|
||||
finally:
|
||||
for key, value in iteritems(old_env):
|
||||
if value is None:
|
||||
try:
|
||||
del os.environ[key]
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
os.environ[key] = value
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
sys.stdin = old_stdin
|
||||
termui.visible_prompt_func = old_visible_prompt_func
|
||||
termui.hidden_prompt_func = old_hidden_prompt_func
|
||||
termui._getchar = old__getchar_func
|
||||
utils.should_strip_ansi = old_should_strip_ansi
|
||||
formatting.FORCED_WIDTH = old_forced_width
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
cli,
|
||||
args=None,
|
||||
input=None,
|
||||
env=None,
|
||||
catch_exceptions=True,
|
||||
color=False,
|
||||
**extra
|
||||
):
|
||||
"""Invokes a command in an isolated environment. The arguments are
|
||||
forwarded directly to the command line script, the `extra` keyword
|
||||
arguments are passed to the :meth:`~clickpkg.Command.main` function of
|
||||
the command.
|
||||
|
||||
This returns a :class:`Result` object.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
The ``catch_exceptions`` parameter was added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
The result object now has an `exc_info` attribute with the
|
||||
traceback if available.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
The ``color`` parameter was added.
|
||||
|
||||
:param cli: the command to invoke
|
||||
:param args: the arguments to invoke. It may be given as an iterable
|
||||
or a string. When given as string it will be interpreted
|
||||
as a Unix shell command. More details at
|
||||
:func:`shlex.split`.
|
||||
:param input: the input data for `sys.stdin`.
|
||||
:param env: the environment overrides.
|
||||
:param catch_exceptions: Whether to catch any other exceptions than
|
||||
``SystemExit``.
|
||||
:param extra: the keyword arguments to pass to :meth:`main`.
|
||||
:param color: whether the output should contain color codes. The
|
||||
application can still override this explicitly.
|
||||
"""
|
||||
exc_info = None
|
||||
with self.isolation(input=input, env=env, color=color) as outstreams:
|
||||
exception = None
|
||||
exit_code = 0
|
||||
|
||||
if isinstance(args, string_types):
|
||||
args = shlex.split(args)
|
||||
|
||||
try:
|
||||
prog_name = extra.pop("prog_name")
|
||||
except KeyError:
|
||||
prog_name = self.get_default_prog_name(cli)
|
||||
|
||||
try:
|
||||
cli.main(args=args or (), prog_name=prog_name, **extra)
|
||||
except SystemExit as e:
|
||||
exc_info = sys.exc_info()
|
||||
exit_code = e.code
|
||||
if exit_code is None:
|
||||
exit_code = 0
|
||||
|
||||
if exit_code != 0:
|
||||
exception = e
|
||||
|
||||
if not isinstance(exit_code, int):
|
||||
sys.stdout.write(str(exit_code))
|
||||
sys.stdout.write("\n")
|
||||
exit_code = 1
|
||||
|
||||
except Exception as e:
|
||||
if not catch_exceptions:
|
||||
raise
|
||||
exception = e
|
||||
exit_code = 1
|
||||
exc_info = sys.exc_info()
|
||||
finally:
|
||||
sys.stdout.flush()
|
||||
stdout = outstreams[0].getvalue()
|
||||
if self.mix_stderr:
|
||||
stderr = None
|
||||
else:
|
||||
stderr = outstreams[1].getvalue()
|
||||
|
||||
return Result(
|
||||
runner=self,
|
||||
stdout_bytes=stdout,
|
||||
stderr_bytes=stderr,
|
||||
exit_code=exit_code,
|
||||
exception=exception,
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def isolated_filesystem(self):
|
||||
"""A context manager that creates a temporary folder and changes
|
||||
the current working directory to it for isolated filesystem tests.
|
||||
"""
|
||||
cwd = os.getcwd()
|
||||
t = tempfile.mkdtemp()
|
||||
os.chdir(t)
|
||||
try:
|
||||
yield t
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
try:
|
||||
shutil.rmtree(t)
|
||||
except (OSError, IOError): # noqa: B014
|
||||
pass
|
||||
@@ -1,762 +0,0 @@
|
||||
import os
|
||||
import stat
|
||||
from datetime import datetime
|
||||
|
||||
from ._compat import _get_argv_encoding
|
||||
from ._compat import filename_to_ui
|
||||
from ._compat import get_filesystem_encoding
|
||||
from ._compat import get_streerror
|
||||
from ._compat import open_stream
|
||||
from ._compat import PY2
|
||||
from ._compat import text_type
|
||||
from .exceptions import BadParameter
|
||||
from .utils import LazyFile
|
||||
from .utils import safecall
|
||||
|
||||
|
||||
class ParamType(object):
|
||||
"""Helper for converting values through types. The following is
|
||||
necessary for a valid type:
|
||||
|
||||
* it needs a name
|
||||
* it needs to pass through None unchanged
|
||||
* it needs to convert from a string
|
||||
* it needs to convert its result type through unchanged
|
||||
(eg: needs to be idempotent)
|
||||
* it needs to be able to deal with param and context being `None`.
|
||||
This can be the case when the object is used with prompt
|
||||
inputs.
|
||||
"""
|
||||
|
||||
is_composite = False
|
||||
|
||||
#: the descriptive name of this type
|
||||
name = None
|
||||
|
||||
#: if a list of this type is expected and the value is pulled from a
|
||||
#: string environment variable, this is what splits it up. `None`
|
||||
#: means any whitespace. For all parameters the general rule is that
|
||||
#: whitespace splits them up. The exception are paths and files which
|
||||
#: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
|
||||
#: Windows).
|
||||
envvar_list_splitter = None
|
||||
|
||||
def __call__(self, value, param=None, ctx=None):
|
||||
if value is not None:
|
||||
return self.convert(value, param, ctx)
|
||||
|
||||
def get_metavar(self, param):
|
||||
"""Returns the metavar default for this param if it provides one."""
|
||||
|
||||
def get_missing_message(self, param):
|
||||
"""Optionally might return extra information about a missing
|
||||
parameter.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
"""Converts the value. This is not invoked for values that are
|
||||
`None` (the missing value).
|
||||
"""
|
||||
return value
|
||||
|
||||
def split_envvar_value(self, rv):
|
||||
"""Given a value from an environment variable this splits it up
|
||||
into small chunks depending on the defined envvar list splitter.
|
||||
|
||||
If the splitter is set to `None`, which means that whitespace splits,
|
||||
then leading and trailing whitespace is ignored. Otherwise, leading
|
||||
and trailing splitters usually lead to empty items being included.
|
||||
"""
|
||||
return (rv or "").split(self.envvar_list_splitter)
|
||||
|
||||
def fail(self, message, param=None, ctx=None):
|
||||
"""Helper method to fail with an invalid value message."""
|
||||
raise BadParameter(message, ctx=ctx, param=param)
|
||||
|
||||
|
||||
class CompositeParamType(ParamType):
|
||||
is_composite = True
|
||||
|
||||
@property
|
||||
def arity(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class FuncParamType(ParamType):
|
||||
def __init__(self, func):
|
||||
self.name = func.__name__
|
||||
self.func = func
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
return self.func(value)
|
||||
except ValueError:
|
||||
try:
|
||||
value = text_type(value)
|
||||
except UnicodeError:
|
||||
value = str(value).decode("utf-8", "replace")
|
||||
self.fail(value, param, ctx)
|
||||
|
||||
|
||||
class UnprocessedParamType(ParamType):
|
||||
name = "text"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
return value
|
||||
|
||||
def __repr__(self):
|
||||
return "UNPROCESSED"
|
||||
|
||||
|
||||
class StringParamType(ParamType):
|
||||
name = "text"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if isinstance(value, bytes):
|
||||
enc = _get_argv_encoding()
|
||||
try:
|
||||
value = value.decode(enc)
|
||||
except UnicodeError:
|
||||
fs_enc = get_filesystem_encoding()
|
||||
if fs_enc != enc:
|
||||
try:
|
||||
value = value.decode(fs_enc)
|
||||
except UnicodeError:
|
||||
value = value.decode("utf-8", "replace")
|
||||
else:
|
||||
value = value.decode("utf-8", "replace")
|
||||
return value
|
||||
return value
|
||||
|
||||
def __repr__(self):
|
||||
return "STRING"
|
||||
|
||||
|
||||
class Choice(ParamType):
|
||||
"""The choice type allows a value to be checked against a fixed set
|
||||
of supported values. All of these values have to be strings.
|
||||
|
||||
You should only pass a list or tuple of choices. Other iterables
|
||||
(like generators) may lead to surprising results.
|
||||
|
||||
The resulting value will always be one of the originally passed choices
|
||||
regardless of ``case_sensitive`` or any ``ctx.token_normalize_func``
|
||||
being specified.
|
||||
|
||||
See :ref:`choice-opts` for an example.
|
||||
|
||||
:param case_sensitive: Set to false to make choices case
|
||||
insensitive. Defaults to true.
|
||||
"""
|
||||
|
||||
name = "choice"
|
||||
|
||||
def __init__(self, choices, case_sensitive=True):
|
||||
self.choices = choices
|
||||
self.case_sensitive = case_sensitive
|
||||
|
||||
def get_metavar(self, param):
|
||||
return "[{}]".format("|".join(self.choices))
|
||||
|
||||
def get_missing_message(self, param):
|
||||
return "Choose from:\n\t{}.".format(",\n\t".join(self.choices))
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
# Match through normalization and case sensitivity
|
||||
# first do token_normalize_func, then lowercase
|
||||
# preserve original `value` to produce an accurate message in
|
||||
# `self.fail`
|
||||
normed_value = value
|
||||
normed_choices = {choice: choice for choice in self.choices}
|
||||
|
||||
if ctx is not None and ctx.token_normalize_func is not None:
|
||||
normed_value = ctx.token_normalize_func(value)
|
||||
normed_choices = {
|
||||
ctx.token_normalize_func(normed_choice): original
|
||||
for normed_choice, original in normed_choices.items()
|
||||
}
|
||||
|
||||
if not self.case_sensitive:
|
||||
if PY2:
|
||||
lower = str.lower
|
||||
else:
|
||||
lower = str.casefold
|
||||
|
||||
normed_value = lower(normed_value)
|
||||
normed_choices = {
|
||||
lower(normed_choice): original
|
||||
for normed_choice, original in normed_choices.items()
|
||||
}
|
||||
|
||||
if normed_value in normed_choices:
|
||||
return normed_choices[normed_value]
|
||||
|
||||
self.fail(
|
||||
"invalid choice: {}. (choose from {})".format(
|
||||
value, ", ".join(self.choices)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "Choice('{}')".format(list(self.choices))
|
||||
|
||||
|
||||
class DateTime(ParamType):
|
||||
"""The DateTime type converts date strings into `datetime` objects.
|
||||
|
||||
The format strings which are checked are configurable, but default to some
|
||||
common (non-timezone aware) ISO 8601 formats.
|
||||
|
||||
When specifying *DateTime* formats, you should only pass a list or a tuple.
|
||||
Other iterables, like generators, may lead to surprising results.
|
||||
|
||||
The format strings are processed using ``datetime.strptime``, and this
|
||||
consequently defines the format strings which are allowed.
|
||||
|
||||
Parsing is tried using each format, in order, and the first format which
|
||||
parses successfully is used.
|
||||
|
||||
:param formats: A list or tuple of date format strings, in the order in
|
||||
which they should be tried. Defaults to
|
||||
``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
|
||||
``'%Y-%m-%d %H:%M:%S'``.
|
||||
"""
|
||||
|
||||
name = "datetime"
|
||||
|
||||
def __init__(self, formats=None):
|
||||
self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
|
||||
|
||||
def get_metavar(self, param):
|
||||
return "[{}]".format("|".join(self.formats))
|
||||
|
||||
def _try_to_convert_date(self, value, format):
|
||||
try:
|
||||
return datetime.strptime(value, format)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
# Exact match
|
||||
for format in self.formats:
|
||||
dtime = self._try_to_convert_date(value, format)
|
||||
if dtime:
|
||||
return dtime
|
||||
|
||||
self.fail(
|
||||
"invalid datetime format: {}. (choose from {})".format(
|
||||
value, ", ".join(self.formats)
|
||||
)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "DateTime"
|
||||
|
||||
|
||||
class IntParamType(ParamType):
|
||||
name = "integer"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
self.fail("{} is not a valid integer".format(value), param, ctx)
|
||||
|
||||
def __repr__(self):
|
||||
return "INT"
|
||||
|
||||
|
||||
class IntRange(IntParamType):
|
||||
"""A parameter that works similar to :data:`click.INT` but restricts
|
||||
the value to fit into a range. The default behavior is to fail if the
|
||||
value falls outside the range, but it can also be silently clamped
|
||||
between the two edges.
|
||||
|
||||
See :ref:`ranges` for an example.
|
||||
"""
|
||||
|
||||
name = "integer range"
|
||||
|
||||
def __init__(self, min=None, max=None, clamp=False):
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.clamp = clamp
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
rv = IntParamType.convert(self, value, param, ctx)
|
||||
if self.clamp:
|
||||
if self.min is not None and rv < self.min:
|
||||
return self.min
|
||||
if self.max is not None and rv > self.max:
|
||||
return self.max
|
||||
if (
|
||||
self.min is not None
|
||||
and rv < self.min
|
||||
or self.max is not None
|
||||
and rv > self.max
|
||||
):
|
||||
if self.min is None:
|
||||
self.fail(
|
||||
"{} is bigger than the maximum valid value {}.".format(
|
||||
rv, self.max
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
elif self.max is None:
|
||||
self.fail(
|
||||
"{} is smaller than the minimum valid value {}.".format(
|
||||
rv, self.min
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
else:
|
||||
self.fail(
|
||||
"{} is not in the valid range of {} to {}.".format(
|
||||
rv, self.min, self.max
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
return rv
|
||||
|
||||
def __repr__(self):
|
||||
return "IntRange({}, {})".format(self.min, self.max)
|
||||
|
||||
|
||||
class FloatParamType(ParamType):
|
||||
name = "float"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
self.fail(
|
||||
"{} is not a valid floating point value".format(value), param, ctx
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "FLOAT"
|
||||
|
||||
|
||||
class FloatRange(FloatParamType):
|
||||
"""A parameter that works similar to :data:`click.FLOAT` but restricts
|
||||
the value to fit into a range. The default behavior is to fail if the
|
||||
value falls outside the range, but it can also be silently clamped
|
||||
between the two edges.
|
||||
|
||||
See :ref:`ranges` for an example.
|
||||
"""
|
||||
|
||||
name = "float range"
|
||||
|
||||
def __init__(self, min=None, max=None, clamp=False):
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.clamp = clamp
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
rv = FloatParamType.convert(self, value, param, ctx)
|
||||
if self.clamp:
|
||||
if self.min is not None and rv < self.min:
|
||||
return self.min
|
||||
if self.max is not None and rv > self.max:
|
||||
return self.max
|
||||
if (
|
||||
self.min is not None
|
||||
and rv < self.min
|
||||
or self.max is not None
|
||||
and rv > self.max
|
||||
):
|
||||
if self.min is None:
|
||||
self.fail(
|
||||
"{} is bigger than the maximum valid value {}.".format(
|
||||
rv, self.max
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
elif self.max is None:
|
||||
self.fail(
|
||||
"{} is smaller than the minimum valid value {}.".format(
|
||||
rv, self.min
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
else:
|
||||
self.fail(
|
||||
"{} is not in the valid range of {} to {}.".format(
|
||||
rv, self.min, self.max
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
return rv
|
||||
|
||||
def __repr__(self):
|
||||
return "FloatRange({}, {})".format(self.min, self.max)
|
||||
|
||||
|
||||
class BoolParamType(ParamType):
|
||||
name = "boolean"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if isinstance(value, bool):
|
||||
return bool(value)
|
||||
value = value.lower()
|
||||
if value in ("true", "t", "1", "yes", "y"):
|
||||
return True
|
||||
elif value in ("false", "f", "0", "no", "n"):
|
||||
return False
|
||||
self.fail("{} is not a valid boolean".format(value), param, ctx)
|
||||
|
||||
def __repr__(self):
|
||||
return "BOOL"
|
||||
|
||||
|
||||
class UUIDParameterType(ParamType):
|
||||
name = "uuid"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
import uuid
|
||||
|
||||
try:
|
||||
if PY2 and isinstance(value, text_type):
|
||||
value = value.encode("ascii")
|
||||
return uuid.UUID(value)
|
||||
except ValueError:
|
||||
self.fail("{} is not a valid UUID value".format(value), param, ctx)
|
||||
|
||||
def __repr__(self):
|
||||
return "UUID"
|
||||
|
||||
|
||||
class File(ParamType):
|
||||
"""Declares a parameter to be a file for reading or writing. The file
|
||||
is automatically closed once the context tears down (after the command
|
||||
finished working).
|
||||
|
||||
Files can be opened for reading or writing. The special value ``-``
|
||||
indicates stdin or stdout depending on the mode.
|
||||
|
||||
By default, the file is opened for reading text data, but it can also be
|
||||
opened in binary mode or for writing. The encoding parameter can be used
|
||||
to force a specific encoding.
|
||||
|
||||
The `lazy` flag controls if the file should be opened immediately or upon
|
||||
first IO. The default is to be non-lazy for standard input and output
|
||||
streams as well as files opened for reading, `lazy` otherwise. When opening a
|
||||
file lazily for reading, it is still opened temporarily for validation, but
|
||||
will not be held open until first IO. lazy is mainly useful when opening
|
||||
for writing to avoid creating the file until it is needed.
|
||||
|
||||
Starting with Click 2.0, files can also be opened atomically in which
|
||||
case all writes go into a separate file in the same folder and upon
|
||||
completion the file will be moved over to the original location. This
|
||||
is useful if a file regularly read by other users is modified.
|
||||
|
||||
See :ref:`file-args` for more information.
|
||||
"""
|
||||
|
||||
name = "filename"
|
||||
envvar_list_splitter = os.path.pathsep
|
||||
|
||||
def __init__(
|
||||
self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False
|
||||
):
|
||||
self.mode = mode
|
||||
self.encoding = encoding
|
||||
self.errors = errors
|
||||
self.lazy = lazy
|
||||
self.atomic = atomic
|
||||
|
||||
def resolve_lazy_flag(self, value):
|
||||
if self.lazy is not None:
|
||||
return self.lazy
|
||||
if value == "-":
|
||||
return False
|
||||
elif "w" in self.mode:
|
||||
return True
|
||||
return False
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
if hasattr(value, "read") or hasattr(value, "write"):
|
||||
return value
|
||||
|
||||
lazy = self.resolve_lazy_flag(value)
|
||||
|
||||
if lazy:
|
||||
f = LazyFile(
|
||||
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||
)
|
||||
if ctx is not None:
|
||||
ctx.call_on_close(f.close_intelligently)
|
||||
return f
|
||||
|
||||
f, should_close = open_stream(
|
||||
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||
)
|
||||
# If a context is provided, we automatically close the file
|
||||
# at the end of the context execution (or flush out). If a
|
||||
# context does not exist, it's the caller's responsibility to
|
||||
# properly close the file. This for instance happens when the
|
||||
# type is used with prompts.
|
||||
if ctx is not None:
|
||||
if should_close:
|
||||
ctx.call_on_close(safecall(f.close))
|
||||
else:
|
||||
ctx.call_on_close(safecall(f.flush))
|
||||
return f
|
||||
except (IOError, OSError) as e: # noqa: B014
|
||||
self.fail(
|
||||
"Could not open file: {}: {}".format(
|
||||
filename_to_ui(value), get_streerror(e)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
|
||||
|
||||
class Path(ParamType):
|
||||
"""The path type is similar to the :class:`File` type but it performs
|
||||
different checks. First of all, instead of returning an open file
|
||||
handle it returns just the filename. Secondly, it can perform various
|
||||
basic checks about what the file or directory should be.
|
||||
|
||||
.. versionchanged:: 6.0
|
||||
`allow_dash` was added.
|
||||
|
||||
:param exists: if set to true, the file or directory needs to exist for
|
||||
this value to be valid. If this is not required and a
|
||||
file does indeed not exist, then all further checks are
|
||||
silently skipped.
|
||||
:param file_okay: controls if a file is a possible value.
|
||||
:param dir_okay: controls if a directory is a possible value.
|
||||
:param writable: if true, a writable check is performed.
|
||||
:param readable: if true, a readable check is performed.
|
||||
:param resolve_path: if this is true, then the path is fully resolved
|
||||
before the value is passed onwards. This means
|
||||
that it's absolute and symlinks are resolved. It
|
||||
will not expand a tilde-prefix, as this is
|
||||
supposed to be done by the shell only.
|
||||
:param allow_dash: If this is set to `True`, a single dash to indicate
|
||||
standard streams is permitted.
|
||||
:param path_type: optionally a string type that should be used to
|
||||
represent the path. The default is `None` which
|
||||
means the return value will be either bytes or
|
||||
unicode depending on what makes most sense given the
|
||||
input data Click deals with.
|
||||
"""
|
||||
|
||||
envvar_list_splitter = os.path.pathsep
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exists=False,
|
||||
file_okay=True,
|
||||
dir_okay=True,
|
||||
writable=False,
|
||||
readable=True,
|
||||
resolve_path=False,
|
||||
allow_dash=False,
|
||||
path_type=None,
|
||||
):
|
||||
self.exists = exists
|
||||
self.file_okay = file_okay
|
||||
self.dir_okay = dir_okay
|
||||
self.writable = writable
|
||||
self.readable = readable
|
||||
self.resolve_path = resolve_path
|
||||
self.allow_dash = allow_dash
|
||||
self.type = path_type
|
||||
|
||||
if self.file_okay and not self.dir_okay:
|
||||
self.name = "file"
|
||||
self.path_type = "File"
|
||||
elif self.dir_okay and not self.file_okay:
|
||||
self.name = "directory"
|
||||
self.path_type = "Directory"
|
||||
else:
|
||||
self.name = "path"
|
||||
self.path_type = "Path"
|
||||
|
||||
def coerce_path_result(self, rv):
|
||||
if self.type is not None and not isinstance(rv, self.type):
|
||||
if self.type is text_type:
|
||||
rv = rv.decode(get_filesystem_encoding())
|
||||
else:
|
||||
rv = rv.encode(get_filesystem_encoding())
|
||||
return rv
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
rv = value
|
||||
|
||||
is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
|
||||
|
||||
if not is_dash:
|
||||
if self.resolve_path:
|
||||
rv = os.path.realpath(rv)
|
||||
|
||||
try:
|
||||
st = os.stat(rv)
|
||||
except OSError:
|
||||
if not self.exists:
|
||||
return self.coerce_path_result(rv)
|
||||
self.fail(
|
||||
"{} '{}' does not exist.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
|
||||
if not self.file_okay and stat.S_ISREG(st.st_mode):
|
||||
self.fail(
|
||||
"{} '{}' is a file.".format(self.path_type, filename_to_ui(value)),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
if not self.dir_okay and stat.S_ISDIR(st.st_mode):
|
||||
self.fail(
|
||||
"{} '{}' is a directory.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
if self.writable and not os.access(value, os.W_OK):
|
||||
self.fail(
|
||||
"{} '{}' is not writable.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
if self.readable and not os.access(value, os.R_OK):
|
||||
self.fail(
|
||||
"{} '{}' is not readable.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
|
||||
return self.coerce_path_result(rv)
|
||||
|
||||
|
||||
class Tuple(CompositeParamType):
|
||||
"""The default behavior of Click is to apply a type on a value directly.
|
||||
This works well in most cases, except for when `nargs` is set to a fixed
|
||||
count and different types should be used for different items. In this
|
||||
case the :class:`Tuple` type can be used. This type can only be used
|
||||
if `nargs` is set to a fixed number.
|
||||
|
||||
For more information see :ref:`tuple-type`.
|
||||
|
||||
This can be selected by using a Python tuple literal as a type.
|
||||
|
||||
:param types: a list of types that should be used for the tuple items.
|
||||
"""
|
||||
|
||||
def __init__(self, types):
|
||||
self.types = [convert_type(ty) for ty in types]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "<{}>".format(" ".join(ty.name for ty in self.types))
|
||||
|
||||
@property
|
||||
def arity(self):
|
||||
return len(self.types)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if len(value) != len(self.types):
|
||||
raise TypeError(
|
||||
"It would appear that nargs is set to conflict with the"
|
||||
" composite type arity."
|
||||
)
|
||||
return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
|
||||
|
||||
|
||||
def convert_type(ty, default=None):
|
||||
"""Converts a callable or python type into the most appropriate
|
||||
param type.
|
||||
"""
|
||||
guessed_type = False
|
||||
if ty is None and default is not None:
|
||||
if isinstance(default, tuple):
|
||||
ty = tuple(map(type, default))
|
||||
else:
|
||||
ty = type(default)
|
||||
guessed_type = True
|
||||
|
||||
if isinstance(ty, tuple):
|
||||
return Tuple(ty)
|
||||
if isinstance(ty, ParamType):
|
||||
return ty
|
||||
if ty is text_type or ty is str or ty is None:
|
||||
return STRING
|
||||
if ty is int:
|
||||
return INT
|
||||
# Booleans are only okay if not guessed. This is done because for
|
||||
# flags the default value is actually a bit of a lie in that it
|
||||
# indicates which of the flags is the one we want. See get_default()
|
||||
# for more information.
|
||||
if ty is bool and not guessed_type:
|
||||
return BOOL
|
||||
if ty is float:
|
||||
return FLOAT
|
||||
if guessed_type:
|
||||
return STRING
|
||||
|
||||
# Catch a common mistake
|
||||
if __debug__:
|
||||
try:
|
||||
if issubclass(ty, ParamType):
|
||||
raise AssertionError(
|
||||
"Attempted to use an uninstantiated parameter type ({}).".format(ty)
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
return FuncParamType(ty)
|
||||
|
||||
|
||||
#: A dummy parameter type that just does nothing. From a user's
|
||||
#: perspective this appears to just be the same as `STRING` but internally
|
||||
#: no string conversion takes place. This is necessary to achieve the
|
||||
#: same bytes/unicode behavior on Python 2/3 in situations where you want
|
||||
#: to not convert argument types. This is usually useful when working
|
||||
#: with file paths as they can appear in bytes and unicode.
|
||||
#:
|
||||
#: For path related uses the :class:`Path` type is a better choice but
|
||||
#: there are situations where an unprocessed type is useful which is why
|
||||
#: it is is provided.
|
||||
#:
|
||||
#: .. versionadded:: 4.0
|
||||
UNPROCESSED = UnprocessedParamType()
|
||||
|
||||
#: A unicode string parameter type which is the implicit default. This
|
||||
#: can also be selected by using ``str`` as type.
|
||||
STRING = StringParamType()
|
||||
|
||||
#: An integer parameter. This can also be selected by using ``int`` as
|
||||
#: type.
|
||||
INT = IntParamType()
|
||||
|
||||
#: A floating point value parameter. This can also be selected by using
|
||||
#: ``float`` as type.
|
||||
FLOAT = FloatParamType()
|
||||
|
||||
#: A boolean parameter. This is the default for boolean flags. This can
|
||||
#: also be selected by using ``bool`` as a type.
|
||||
BOOL = BoolParamType()
|
||||
|
||||
#: A UUID parameter.
|
||||
UUID = UUIDParameterType()
|
||||
@@ -1,455 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ._compat import _default_text_stderr
|
||||
from ._compat import _default_text_stdout
|
||||
from ._compat import auto_wrap_for_ansi
|
||||
from ._compat import binary_streams
|
||||
from ._compat import filename_to_ui
|
||||
from ._compat import get_filesystem_encoding
|
||||
from ._compat import get_streerror
|
||||
from ._compat import is_bytes
|
||||
from ._compat import open_stream
|
||||
from ._compat import PY2
|
||||
from ._compat import should_strip_ansi
|
||||
from ._compat import string_types
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import text_streams
|
||||
from ._compat import text_type
|
||||
from ._compat import WIN
|
||||
from .globals import resolve_color_default
|
||||
|
||||
if not PY2:
|
||||
from ._compat import _find_binary_writer
|
||||
elif WIN:
|
||||
from ._winconsole import _get_windows_argv
|
||||
from ._winconsole import _hash_py_argv
|
||||
from ._winconsole import _initial_argv_hash
|
||||
|
||||
echo_native_types = string_types + (bytes, bytearray)
|
||||
|
||||
|
||||
def _posixify(name):
|
||||
return "-".join(name.split()).lower()
|
||||
|
||||
|
||||
def safecall(func):
|
||||
"""Wraps a function so that it swallows exceptions."""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def make_str(value):
|
||||
"""Converts a value into a valid string."""
|
||||
if isinstance(value, bytes):
|
||||
try:
|
||||
return value.decode(get_filesystem_encoding())
|
||||
except UnicodeError:
|
||||
return value.decode("utf-8", "replace")
|
||||
return text_type(value)
|
||||
|
||||
|
||||
def make_default_short_help(help, max_length=45):
|
||||
"""Return a condensed version of help string."""
|
||||
words = help.split()
|
||||
total_length = 0
|
||||
result = []
|
||||
done = False
|
||||
|
||||
for word in words:
|
||||
if word[-1:] == ".":
|
||||
done = True
|
||||
new_length = 1 + len(word) if result else len(word)
|
||||
if total_length + new_length > max_length:
|
||||
result.append("...")
|
||||
done = True
|
||||
else:
|
||||
if result:
|
||||
result.append(" ")
|
||||
result.append(word)
|
||||
if done:
|
||||
break
|
||||
total_length += new_length
|
||||
|
||||
return "".join(result)
|
||||
|
||||
|
||||
class LazyFile(object):
|
||||
"""A lazy file works like a regular file but it does not fully open
|
||||
the file but it does perform some basic checks early to see if the
|
||||
filename parameter does make sense. This is useful for safely opening
|
||||
files for writing.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, filename, mode="r", encoding=None, errors="strict", atomic=False
|
||||
):
|
||||
self.name = filename
|
||||
self.mode = mode
|
||||
self.encoding = encoding
|
||||
self.errors = errors
|
||||
self.atomic = atomic
|
||||
|
||||
if filename == "-":
|
||||
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
|
||||
else:
|
||||
if "r" in mode:
|
||||
# Open and close the file in case we're opening it for
|
||||
# reading so that we can catch at least some errors in
|
||||
# some cases early.
|
||||
open(filename, mode).close()
|
||||
self._f = None
|
||||
self.should_close = True
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.open(), name)
|
||||
|
||||
def __repr__(self):
|
||||
if self._f is not None:
|
||||
return repr(self._f)
|
||||
return "<unopened file '{}' {}>".format(self.name, self.mode)
|
||||
|
||||
def open(self):
|
||||
"""Opens the file if it's not yet open. This call might fail with
|
||||
a :exc:`FileError`. Not handling this error will produce an error
|
||||
that Click shows.
|
||||
"""
|
||||
if self._f is not None:
|
||||
return self._f
|
||||
try:
|
||||
rv, self.should_close = open_stream(
|
||||
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||
)
|
||||
except (IOError, OSError) as e: # noqa: E402
|
||||
from .exceptions import FileError
|
||||
|
||||
raise FileError(self.name, hint=get_streerror(e))
|
||||
self._f = rv
|
||||
return rv
|
||||
|
||||
def close(self):
|
||||
"""Closes the underlying file, no matter what."""
|
||||
if self._f is not None:
|
||||
self._f.close()
|
||||
|
||||
def close_intelligently(self):
|
||||
"""This function only closes the file if it was opened by the lazy
|
||||
file wrapper. For instance this will never close stdin.
|
||||
"""
|
||||
if self.should_close:
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
self.close_intelligently()
|
||||
|
||||
def __iter__(self):
|
||||
self.open()
|
||||
return iter(self._f)
|
||||
|
||||
|
||||
class KeepOpenFile(object):
|
||||
def __init__(self, file):
|
||||
self._file = file
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._file, name)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._file)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._file)
|
||||
|
||||
|
||||
def echo(message=None, file=None, nl=True, err=False, color=None):
|
||||
"""Prints a message plus a newline to the given file or stdout. On
|
||||
first sight, this looks like the print function, but it has improved
|
||||
support for handling Unicode and binary data that does not fail no
|
||||
matter how badly configured the system is.
|
||||
|
||||
Primarily it means that you can print binary data as well as Unicode
|
||||
data on both 2.x and 3.x to the given file in the most appropriate way
|
||||
possible. This is a very carefree function in that it will try its
|
||||
best to not fail. As of Click 6.0 this includes support for unicode
|
||||
output on the Windows console.
|
||||
|
||||
In addition to that, if `colorama`_ is installed, the echo function will
|
||||
also support clever handling of ANSI codes. Essentially it will then
|
||||
do the following:
|
||||
|
||||
- add transparent handling of ANSI color codes on Windows.
|
||||
- hide ANSI codes automatically if the destination file is not a
|
||||
terminal.
|
||||
|
||||
.. _colorama: https://pypi.org/project/colorama/
|
||||
|
||||
.. versionchanged:: 6.0
|
||||
As of Click 6.0 the echo function will properly support unicode
|
||||
output on the windows console. Not that click does not modify
|
||||
the interpreter in any way which means that `sys.stdout` or the
|
||||
print statement or function will still not provide unicode support.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
Starting with version 2.0 of Click, the echo function will work
|
||||
with colorama if it's installed.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
The `err` parameter was added.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
Added the `color` flag.
|
||||
|
||||
:param message: the message to print
|
||||
:param file: the file to write to (defaults to ``stdout``)
|
||||
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||
``stdout``. This is faster and easier than calling
|
||||
:func:`get_text_stderr` yourself.
|
||||
:param nl: if set to `True` (the default) a newline is printed afterwards.
|
||||
:param color: controls if the terminal supports ANSI colors or not. The
|
||||
default is autodetection.
|
||||
"""
|
||||
if file is None:
|
||||
if err:
|
||||
file = _default_text_stderr()
|
||||
else:
|
||||
file = _default_text_stdout()
|
||||
|
||||
# Convert non bytes/text into the native string type.
|
||||
if message is not None and not isinstance(message, echo_native_types):
|
||||
message = text_type(message)
|
||||
|
||||
if nl:
|
||||
message = message or u""
|
||||
if isinstance(message, text_type):
|
||||
message += u"\n"
|
||||
else:
|
||||
message += b"\n"
|
||||
|
||||
# If there is a message, and we're in Python 3, and the value looks
|
||||
# like bytes, we manually need to find the binary stream and write the
|
||||
# message in there. This is done separately so that most stream
|
||||
# types will work as you would expect. Eg: you can write to StringIO
|
||||
# for other cases.
|
||||
if message and not PY2 and is_bytes(message):
|
||||
binary_file = _find_binary_writer(file)
|
||||
if binary_file is not None:
|
||||
file.flush()
|
||||
binary_file.write(message)
|
||||
binary_file.flush()
|
||||
return
|
||||
|
||||
# ANSI-style support. If there is no message or we are dealing with
|
||||
# bytes nothing is happening. If we are connected to a file we want
|
||||
# to strip colors. If we are on windows we either wrap the stream
|
||||
# to strip the color or we use the colorama support to translate the
|
||||
# ansi codes to API calls.
|
||||
if message and not is_bytes(message):
|
||||
color = resolve_color_default(color)
|
||||
if should_strip_ansi(file, color):
|
||||
message = strip_ansi(message)
|
||||
elif WIN:
|
||||
if auto_wrap_for_ansi is not None:
|
||||
file = auto_wrap_for_ansi(file)
|
||||
elif not color:
|
||||
message = strip_ansi(message)
|
||||
|
||||
if message:
|
||||
file.write(message)
|
||||
file.flush()
|
||||
|
||||
|
||||
def get_binary_stream(name):
|
||||
"""Returns a system stream for byte processing. This essentially
|
||||
returns the stream from the sys module with the given name but it
|
||||
solves some compatibility issues between different Python versions.
|
||||
Primarily this function is necessary for getting binary streams on
|
||||
Python 3.
|
||||
|
||||
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||
``'stdout'`` and ``'stderr'``
|
||||
"""
|
||||
opener = binary_streams.get(name)
|
||||
if opener is None:
|
||||
raise TypeError("Unknown standard stream '{}'".format(name))
|
||||
return opener()
|
||||
|
||||
|
||||
def get_text_stream(name, encoding=None, errors="strict"):
|
||||
"""Returns a system stream for text processing. This usually returns
|
||||
a wrapped stream around a binary stream returned from
|
||||
:func:`get_binary_stream` but it also can take shortcuts on Python 3
|
||||
for already correctly configured streams.
|
||||
|
||||
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||
``'stdout'`` and ``'stderr'``
|
||||
:param encoding: overrides the detected default encoding.
|
||||
:param errors: overrides the default error mode.
|
||||
"""
|
||||
opener = text_streams.get(name)
|
||||
if opener is None:
|
||||
raise TypeError("Unknown standard stream '{}'".format(name))
|
||||
return opener(encoding, errors)
|
||||
|
||||
|
||||
def open_file(
|
||||
filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False
|
||||
):
|
||||
"""This is similar to how the :class:`File` works but for manual
|
||||
usage. Files are opened non lazy by default. This can open regular
|
||||
files as well as stdin/stdout if ``'-'`` is passed.
|
||||
|
||||
If stdin/stdout is returned the stream is wrapped so that the context
|
||||
manager will not close the stream accidentally. This makes it possible
|
||||
to always use the function like this without having to worry to
|
||||
accidentally close a standard stream::
|
||||
|
||||
with open_file(filename) as f:
|
||||
...
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
:param filename: the name of the file to open (or ``'-'`` for stdin/stdout).
|
||||
:param mode: the mode in which to open the file.
|
||||
:param encoding: the encoding to use.
|
||||
:param errors: the error handling for this file.
|
||||
:param lazy: can be flipped to true to open the file lazily.
|
||||
:param atomic: in atomic mode writes go into a temporary file and it's
|
||||
moved on close.
|
||||
"""
|
||||
if lazy:
|
||||
return LazyFile(filename, mode, encoding, errors, atomic=atomic)
|
||||
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
|
||||
if not should_close:
|
||||
f = KeepOpenFile(f)
|
||||
return f
|
||||
|
||||
|
||||
def get_os_args():
|
||||
"""This returns the argument part of sys.argv in the most appropriate
|
||||
form for processing. What this means is that this return value is in
|
||||
a format that works for Click to process but does not necessarily
|
||||
correspond well to what's actually standard for the interpreter.
|
||||
|
||||
On most environments the return value is ``sys.argv[:1]`` unchanged.
|
||||
However if you are on Windows and running Python 2 the return value
|
||||
will actually be a list of unicode strings instead because the
|
||||
default behavior on that platform otherwise will not be able to
|
||||
carry all possible values that sys.argv can have.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
"""
|
||||
# We can only extract the unicode argv if sys.argv has not been
|
||||
# changed since the startup of the application.
|
||||
if PY2 and WIN and _initial_argv_hash == _hash_py_argv():
|
||||
return _get_windows_argv()
|
||||
return sys.argv[1:]
|
||||
|
||||
|
||||
def format_filename(filename, shorten=False):
|
||||
"""Formats a filename for user display. The main purpose of this
|
||||
function is to ensure that the filename can be displayed at all. This
|
||||
will decode the filename to unicode if necessary in a way that it will
|
||||
not fail. Optionally, it can shorten the filename to not include the
|
||||
full path to the filename.
|
||||
|
||||
:param filename: formats a filename for UI display. This will also convert
|
||||
the filename into unicode without failing.
|
||||
:param shorten: this optionally shortens the filename to strip of the
|
||||
path that leads up to it.
|
||||
"""
|
||||
if shorten:
|
||||
filename = os.path.basename(filename)
|
||||
return filename_to_ui(filename)
|
||||
|
||||
|
||||
def get_app_dir(app_name, roaming=True, force_posix=False):
|
||||
r"""Returns the config folder for the application. The default behavior
|
||||
is to return whatever is most appropriate for the operating system.
|
||||
|
||||
To give you an idea, for an app called ``"Foo Bar"``, something like
|
||||
the following folders could be returned:
|
||||
|
||||
Mac OS X:
|
||||
``~/Library/Application Support/Foo Bar``
|
||||
Mac OS X (POSIX):
|
||||
``~/.foo-bar``
|
||||
Unix:
|
||||
``~/.config/foo-bar``
|
||||
Unix (POSIX):
|
||||
``~/.foo-bar``
|
||||
Win XP (roaming):
|
||||
``C:\Documents and Settings\<user>\Local Settings\Application Data\Foo Bar``
|
||||
Win XP (not roaming):
|
||||
``C:\Documents and Settings\<user>\Application Data\Foo Bar``
|
||||
Win 7 (roaming):
|
||||
``C:\Users\<user>\AppData\Roaming\Foo Bar``
|
||||
Win 7 (not roaming):
|
||||
``C:\Users\<user>\AppData\Local\Foo Bar``
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
:param app_name: the application name. This should be properly capitalized
|
||||
and can contain whitespace.
|
||||
:param roaming: controls if the folder should be roaming or not on Windows.
|
||||
Has no affect otherwise.
|
||||
:param force_posix: if this is set to `True` then on any POSIX system the
|
||||
folder will be stored in the home folder with a leading
|
||||
dot instead of the XDG config home or darwin's
|
||||
application support folder.
|
||||
"""
|
||||
if WIN:
|
||||
key = "APPDATA" if roaming else "LOCALAPPDATA"
|
||||
folder = os.environ.get(key)
|
||||
if folder is None:
|
||||
folder = os.path.expanduser("~")
|
||||
return os.path.join(folder, app_name)
|
||||
if force_posix:
|
||||
return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name))))
|
||||
if sys.platform == "darwin":
|
||||
return os.path.join(
|
||||
os.path.expanduser("~/Library/Application Support"), app_name
|
||||
)
|
||||
return os.path.join(
|
||||
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
||||
_posixify(app_name),
|
||||
)
|
||||
|
||||
|
||||
class PacifyFlushWrapper(object):
|
||||
"""This wrapper is used to catch and suppress BrokenPipeErrors resulting
|
||||
from ``.flush()`` being called on broken pipe during the shutdown/final-GC
|
||||
of the Python interpreter. Notably ``.flush()`` is always called on
|
||||
``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
|
||||
other cleanup code, and the case where the underlying file is not a broken
|
||||
pipe, all calls and attributes are proxied.
|
||||
"""
|
||||
|
||||
def __init__(self, wrapped):
|
||||
self.wrapped = wrapped
|
||||
|
||||
def flush(self):
|
||||
try:
|
||||
self.wrapped.flush()
|
||||
except IOError as e:
|
||||
import errno
|
||||
|
||||
if e.errno != errno.EPIPE:
|
||||
raise
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.wrapped, attr)
|
||||
@@ -1,46 +0,0 @@
|
||||
from .compat import IS_TYPE_CHECKING
|
||||
from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values
|
||||
|
||||
if IS_TYPE_CHECKING:
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def load_ipython_extension(ipython):
|
||||
# type: (Any) -> None
|
||||
from .ipython import load_ipython_extension
|
||||
load_ipython_extension(ipython)
|
||||
|
||||
|
||||
def get_cli_string(path=None, action=None, key=None, value=None, quote=None):
|
||||
# type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str
|
||||
"""Returns a string suitable for running as a shell script.
|
||||
|
||||
Useful for converting a arguments passed to a fabric task
|
||||
to be passed to a `local` or `run` command.
|
||||
"""
|
||||
command = ['dotenv']
|
||||
if quote:
|
||||
command.append('-q %s' % quote)
|
||||
if path:
|
||||
command.append('-f %s' % path)
|
||||
if action:
|
||||
command.append(action)
|
||||
if key:
|
||||
command.append(key)
|
||||
if value:
|
||||
if ' ' in value:
|
||||
command.append('"%s"' % value)
|
||||
else:
|
||||
command.append(value)
|
||||
|
||||
return ' '.join(command).strip()
|
||||
|
||||
|
||||
__all__ = ['get_cli_string',
|
||||
'load_dotenv',
|
||||
'dotenv_values',
|
||||
'get_key',
|
||||
'set_key',
|
||||
'unset_key',
|
||||
'find_dotenv',
|
||||
'load_ipython_extension']
|
||||
@@ -1,174 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from subprocess import Popen
|
||||
|
||||
try:
|
||||
import click
|
||||
except ImportError:
|
||||
sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
|
||||
'Run pip install "python-dotenv[cli]" to fix this.')
|
||||
sys.exit(1)
|
||||
|
||||
from .compat import IS_TYPE_CHECKING, to_env
|
||||
from .main import dotenv_values, get_key, set_key, unset_key
|
||||
from .version import __version__
|
||||
|
||||
if IS_TYPE_CHECKING:
|
||||
from typing import Any, List, Dict
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
|
||||
type=click.Path(file_okay=True),
|
||||
help="Location of the .env file, defaults to .env file in current working directory.")
|
||||
@click.option('-q', '--quote', default='always',
|
||||
type=click.Choice(['always', 'never', 'auto']),
|
||||
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
|
||||
@click.option('-e', '--export', default=False,
|
||||
type=click.BOOL,
|
||||
help="Whether to write the dot file as an executable bash script.")
|
||||
@click.version_option(version=__version__)
|
||||
@click.pass_context
|
||||
def cli(ctx, file, quote, export):
|
||||
# type: (click.Context, Any, Any, Any) -> None
|
||||
'''This script is used to set, get or unset values from a .env file.'''
|
||||
ctx.obj = {}
|
||||
ctx.obj['QUOTE'] = quote
|
||||
ctx.obj['EXPORT'] = export
|
||||
ctx.obj['FILE'] = file
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def list(ctx):
|
||||
# type: (click.Context) -> None
|
||||
'''Display all the stored key/value.'''
|
||||
file = ctx.obj['FILE']
|
||||
if not os.path.isfile(file):
|
||||
raise click.BadParameter(
|
||||
'Path "%s" does not exist.' % (file),
|
||||
ctx=ctx
|
||||
)
|
||||
dotenv_as_dict = dotenv_values(file)
|
||||
for k, v in dotenv_as_dict.items():
|
||||
click.echo('%s=%s' % (k, v))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
@click.argument('key', required=True)
|
||||
@click.argument('value', required=True)
|
||||
def set(ctx, key, value):
|
||||
# type: (click.Context, Any, Any) -> None
|
||||
'''Store the given key/value.'''
|
||||
file = ctx.obj['FILE']
|
||||
quote = ctx.obj['QUOTE']
|
||||
export = ctx.obj['EXPORT']
|
||||
success, key, value = set_key(file, key, value, quote, export)
|
||||
if success:
|
||||
click.echo('%s=%s' % (key, value))
|
||||
else:
|
||||
exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
@click.argument('key', required=True)
|
||||
def get(ctx, key):
|
||||
# type: (click.Context, Any) -> None
|
||||
'''Retrieve the value for the given key.'''
|
||||
file = ctx.obj['FILE']
|
||||
if not os.path.isfile(file):
|
||||
raise click.BadParameter(
|
||||
'Path "%s" does not exist.' % (file),
|
||||
ctx=ctx
|
||||
)
|
||||
stored_value = get_key(file, key)
|
||||
if stored_value:
|
||||
click.echo(stored_value)
|
||||
else:
|
||||
exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
@click.argument('key', required=True)
|
||||
def unset(ctx, key):
|
||||
# type: (click.Context, Any) -> None
|
||||
'''Removes the given key.'''
|
||||
file = ctx.obj['FILE']
|
||||
quote = ctx.obj['QUOTE']
|
||||
success, key = unset_key(file, key, quote)
|
||||
if success:
|
||||
click.echo("Successfully removed %s" % key)
|
||||
else:
|
||||
exit(1)
|
||||
|
||||
|
||||
@cli.command(context_settings={'ignore_unknown_options': True})
|
||||
@click.pass_context
|
||||
@click.option(
|
||||
"--override/--no-override",
|
||||
default=True,
|
||||
help="Override variables from the environment file with those from the .env file.",
|
||||
)
|
||||
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
|
||||
def run(ctx, override, commandline):
|
||||
# type: (click.Context, bool, List[str]) -> None
|
||||
"""Run command with environment variables present."""
|
||||
file = ctx.obj['FILE']
|
||||
if not os.path.isfile(file):
|
||||
raise click.BadParameter(
|
||||
'Invalid value for \'-f\' "%s" does not exist.' % (file),
|
||||
ctx=ctx
|
||||
)
|
||||
dotenv_as_dict = {
|
||||
to_env(k): to_env(v)
|
||||
for (k, v) in dotenv_values(file).items()
|
||||
if v is not None and (override or to_env(k) not in os.environ)
|
||||
}
|
||||
|
||||
if not commandline:
|
||||
click.echo('No command given.')
|
||||
exit(1)
|
||||
ret = run_command(commandline, dotenv_as_dict)
|
||||
exit(ret)
|
||||
|
||||
|
||||
def run_command(command, env):
|
||||
# type: (List[str], Dict[str, str]) -> int
|
||||
"""Run command in sub process.
|
||||
|
||||
Runs the command in a sub process with the variables from `env`
|
||||
added in the current environment variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command: List[str]
|
||||
The command and it's parameters
|
||||
env: Dict
|
||||
The additional environment variables
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The return code of the command
|
||||
|
||||
"""
|
||||
# copy the current environment variables and add the vales from
|
||||
# `env`
|
||||
cmd_env = os.environ.copy()
|
||||
cmd_env.update(env)
|
||||
|
||||
p = Popen(command,
|
||||
universal_newlines=True,
|
||||
bufsize=0,
|
||||
shell=False,
|
||||
env=cmd_env)
|
||||
_, _ = p.communicate()
|
||||
|
||||
return p.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -1,49 +0,0 @@
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2 # type: bool
|
||||
|
||||
if PY2:
|
||||
from StringIO import StringIO # noqa
|
||||
else:
|
||||
from io import StringIO # noqa
|
||||
|
||||
|
||||
def is_type_checking():
|
||||
# type: () -> bool
|
||||
try:
|
||||
from typing import TYPE_CHECKING
|
||||
except ImportError:
|
||||
return False
|
||||
return TYPE_CHECKING
|
||||
|
||||
|
||||
IS_TYPE_CHECKING = is_type_checking()
|
||||
|
||||
|
||||
if IS_TYPE_CHECKING:
|
||||
from typing import Text
|
||||
|
||||
|
||||
def to_env(text):
|
||||
# type: (Text) -> str
|
||||
"""
|
||||
Encode a string the same way whether it comes from the environment or a `.env` file.
|
||||
"""
|
||||
if PY2:
|
||||
return text.encode(sys.getfilesystemencoding() or "utf-8")
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def to_text(string):
|
||||
# type: (str) -> Text
|
||||
"""
|
||||
Make a string Unicode if it isn't already.
|
||||
|
||||
This is useful for defining raw unicode strings because `ur"foo"` isn't valid in
|
||||
Python 3.
|
||||
"""
|
||||
if PY2:
|
||||
return string.decode("utf-8")
|
||||
else:
|
||||
return string
|
||||
@@ -1,41 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
|
||||
from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
|
||||
parse_argstring) # type: ignore
|
||||
|
||||
from .main import find_dotenv, load_dotenv
|
||||
|
||||
|
||||
@magics_class
|
||||
class IPythonDotEnv(Magics):
|
||||
|
||||
@magic_arguments()
|
||||
@argument(
|
||||
'-o', '--override', action='store_true',
|
||||
help="Indicate to override existing variables"
|
||||
)
|
||||
@argument(
|
||||
'-v', '--verbose', action='store_true',
|
||||
help="Indicate function calls to be verbose"
|
||||
)
|
||||
@argument('dotenv_path', nargs='?', type=str, default='.env',
|
||||
help='Search in increasingly higher folders for the `dotenv_path`')
|
||||
@line_magic
|
||||
def dotenv(self, line):
|
||||
args = parse_argstring(self.dotenv, line)
|
||||
# Locate the .env file
|
||||
dotenv_path = args.dotenv_path
|
||||
try:
|
||||
dotenv_path = find_dotenv(dotenv_path, True, True)
|
||||
except IOError:
|
||||
print("cannot find .env file")
|
||||
return
|
||||
|
||||
# Load the .env file
|
||||
load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)
|
||||
|
||||
|
||||
def load_ipython_extension(ipython):
|
||||
"""Register the %dotenv magic."""
|
||||
ipython.register_magics(IPythonDotEnv)
|
||||
@@ -1,355 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .compat import IS_TYPE_CHECKING, PY2, StringIO, to_env
|
||||
from .parser import Binding, parse_stream
|
||||
from .variables import parse_variables
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if IS_TYPE_CHECKING:
|
||||
from typing import (IO, Dict, Iterable, Iterator, Mapping, Optional, Text,
|
||||
Tuple, Union)
|
||||
if sys.version_info >= (3, 6):
|
||||
_PathLike = os.PathLike
|
||||
else:
|
||||
_PathLike = Text
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
_StringIO = StringIO
|
||||
else:
|
||||
_StringIO = StringIO[Text]
|
||||
|
||||
|
||||
def with_warn_for_invalid_lines(mappings):
|
||||
# type: (Iterator[Binding]) -> Iterator[Binding]
|
||||
for mapping in mappings:
|
||||
if mapping.error:
|
||||
logger.warning(
|
||||
"Python-dotenv could not parse statement starting at line %s",
|
||||
mapping.original.line,
|
||||
)
|
||||
yield mapping
|
||||
|
||||
|
||||
class DotEnv():
|
||||
|
||||
def __init__(self, dotenv_path, verbose=False, encoding=None, interpolate=True, override=True):
|
||||
# type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text], bool, bool) -> None
|
||||
self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO]
|
||||
self._dict = None # type: Optional[Dict[Text, Optional[Text]]]
|
||||
self.verbose = verbose # type: bool
|
||||
self.encoding = encoding # type: Union[None, Text]
|
||||
self.interpolate = interpolate # type: bool
|
||||
self.override = override # type: bool
|
||||
|
||||
@contextmanager
|
||||
def _get_stream(self):
|
||||
# type: () -> Iterator[IO[Text]]
|
||||
if isinstance(self.dotenv_path, StringIO):
|
||||
yield self.dotenv_path
|
||||
elif os.path.isfile(self.dotenv_path):
|
||||
with io.open(self.dotenv_path, encoding=self.encoding) as stream:
|
||||
yield stream
|
||||
else:
|
||||
if self.verbose:
|
||||
logger.info("Python-dotenv could not find configuration file %s.", self.dotenv_path or '.env')
|
||||
yield StringIO('')
|
||||
|
||||
def dict(self):
|
||||
# type: () -> Dict[Text, Optional[Text]]
|
||||
"""Return dotenv as dict"""
|
||||
if self._dict:
|
||||
return self._dict
|
||||
|
||||
raw_values = self.parse()
|
||||
|
||||
if self.interpolate:
|
||||
self._dict = OrderedDict(resolve_variables(raw_values, override=self.override))
|
||||
else:
|
||||
self._dict = OrderedDict(raw_values)
|
||||
|
||||
return self._dict
|
||||
|
||||
def parse(self):
|
||||
# type: () -> Iterator[Tuple[Text, Optional[Text]]]
|
||||
with self._get_stream() as stream:
|
||||
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
|
||||
if mapping.key is not None:
|
||||
yield mapping.key, mapping.value
|
||||
|
||||
def set_as_environment_variables(self):
|
||||
# type: () -> bool
|
||||
"""
|
||||
Load the current dotenv as system environment variable.
|
||||
"""
|
||||
for k, v in self.dict().items():
|
||||
if k in os.environ and not self.override:
|
||||
continue
|
||||
if v is not None:
|
||||
os.environ[to_env(k)] = to_env(v)
|
||||
|
||||
return True
|
||||
|
||||
def get(self, key):
|
||||
# type: (Text) -> Optional[Text]
|
||||
"""
|
||||
"""
|
||||
data = self.dict()
|
||||
|
||||
if key in data:
|
||||
return data[key]
|
||||
|
||||
if self.verbose:
|
||||
logger.warning("Key %s not found in %s.", key, self.dotenv_path)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_key(dotenv_path, key_to_get):
|
||||
# type: (Union[Text, _PathLike], Text) -> Optional[Text]
|
||||
"""
|
||||
Gets the value of a given key from the given .env
|
||||
|
||||
If the .env path given doesn't exist, fails
|
||||
"""
|
||||
return DotEnv(dotenv_path, verbose=True).get(key_to_get)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def rewrite(path):
|
||||
# type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
|
||||
try:
|
||||
if not os.path.isfile(path):
|
||||
with io.open(path, "w+") as source:
|
||||
source.write("")
|
||||
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
|
||||
with io.open(path) as source:
|
||||
yield (source, dest) # type: ignore
|
||||
except BaseException:
|
||||
if os.path.isfile(dest.name):
|
||||
os.unlink(dest.name)
|
||||
raise
|
||||
else:
|
||||
shutil.move(dest.name, path)
|
||||
|
||||
|
||||
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always", export=False):
|
||||
# type: (_PathLike, Text, Text, Text, bool) -> Tuple[Optional[bool], Text, Text]
|
||||
"""
|
||||
Adds or Updates a key/value to the given .env
|
||||
|
||||
If the .env path given doesn't exist, fails instead of risking creating
|
||||
an orphan .env somewhere in the filesystem
|
||||
"""
|
||||
value_to_set = value_to_set.strip("'").strip('"')
|
||||
|
||||
if " " in value_to_set:
|
||||
quote_mode = "always"
|
||||
|
||||
if quote_mode == "always":
|
||||
value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
|
||||
else:
|
||||
value_out = value_to_set
|
||||
if export:
|
||||
line_out = 'export {}={}\n'.format(key_to_set, value_out)
|
||||
else:
|
||||
line_out = "{}={}\n".format(key_to_set, value_out)
|
||||
|
||||
with rewrite(dotenv_path) as (source, dest):
|
||||
replaced = False
|
||||
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
|
||||
if mapping.key == key_to_set:
|
||||
dest.write(line_out)
|
||||
replaced = True
|
||||
else:
|
||||
dest.write(mapping.original.string)
|
||||
if not replaced:
|
||||
dest.write(line_out)
|
||||
|
||||
return True, key_to_set, value_to_set
|
||||
|
||||
|
||||
def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
|
||||
# type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text]
|
||||
"""
|
||||
Removes a given key from the given .env
|
||||
|
||||
If the .env path given doesn't exist, fails
|
||||
If the given key doesn't exist in the .env, fails
|
||||
"""
|
||||
if not os.path.exists(dotenv_path):
|
||||
logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
|
||||
return None, key_to_unset
|
||||
|
||||
removed = False
|
||||
with rewrite(dotenv_path) as (source, dest):
|
||||
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
|
||||
if mapping.key == key_to_unset:
|
||||
removed = True
|
||||
else:
|
||||
dest.write(mapping.original.string)
|
||||
|
||||
if not removed:
|
||||
logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path)
|
||||
return None, key_to_unset
|
||||
|
||||
return removed, key_to_unset
|
||||
|
||||
|
||||
def resolve_variables(values, override):
|
||||
# type: (Iterable[Tuple[Text, Optional[Text]]], bool) -> Mapping[Text, Optional[Text]]
|
||||
|
||||
new_values = {} # type: Dict[Text, Optional[Text]]
|
||||
|
||||
for (name, value) in values:
|
||||
if value is None:
|
||||
result = None
|
||||
else:
|
||||
atoms = parse_variables(value)
|
||||
env = {} # type: Dict[Text, Optional[Text]]
|
||||
if override:
|
||||
env.update(os.environ) # type: ignore
|
||||
env.update(new_values)
|
||||
else:
|
||||
env.update(new_values)
|
||||
env.update(os.environ) # type: ignore
|
||||
result = "".join(atom.resolve(env) for atom in atoms)
|
||||
|
||||
new_values[name] = result
|
||||
|
||||
return new_values
|
||||
|
||||
|
||||
def _walk_to_root(path):
|
||||
# type: (Text) -> Iterator[Text]
|
||||
"""
|
||||
Yield directories starting from the given directory up to the root
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise IOError('Starting path not found')
|
||||
|
||||
if os.path.isfile(path):
|
||||
path = os.path.dirname(path)
|
||||
|
||||
last_dir = None
|
||||
current_dir = os.path.abspath(path)
|
||||
while last_dir != current_dir:
|
||||
yield current_dir
|
||||
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
|
||||
last_dir, current_dir = current_dir, parent_dir
|
||||
|
||||
|
||||
def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
|
||||
# type: (Text, bool, bool) -> Text
|
||||
"""
|
||||
Search in increasingly higher folders for the given file
|
||||
|
||||
Returns path to the file if found, or an empty string otherwise
|
||||
"""
|
||||
|
||||
def _is_interactive():
|
||||
""" Decide whether this is running in a REPL or IPython notebook """
|
||||
main = __import__('__main__', None, None, fromlist=['__file__'])
|
||||
return not hasattr(main, '__file__')
|
||||
|
||||
if usecwd or _is_interactive() or getattr(sys, 'frozen', False):
|
||||
# Should work without __file__, e.g. in REPL or IPython notebook.
|
||||
path = os.getcwd()
|
||||
else:
|
||||
# will work for .py files
|
||||
frame = sys._getframe()
|
||||
# find first frame that is outside of this file
|
||||
if PY2 and not __file__.endswith('.py'):
|
||||
# in Python2 __file__ extension could be .pyc or .pyo (this doesn't account
|
||||
# for edge case of Python compiled for non-standard extension)
|
||||
current_file = __file__.rsplit('.', 1)[0] + '.py'
|
||||
else:
|
||||
current_file = __file__
|
||||
|
||||
while frame.f_code.co_filename == current_file:
|
||||
assert frame.f_back is not None
|
||||
frame = frame.f_back
|
||||
frame_filename = frame.f_code.co_filename
|
||||
path = os.path.dirname(os.path.abspath(frame_filename))
|
||||
|
||||
for dirname in _walk_to_root(path):
|
||||
check_path = os.path.join(dirname, filename)
|
||||
if os.path.isfile(check_path):
|
||||
return check_path
|
||||
|
||||
if raise_error_if_not_found:
|
||||
raise IOError('File not found')
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
def load_dotenv(
|
||||
dotenv_path=None,
|
||||
stream=None,
|
||||
verbose=False,
|
||||
override=False,
|
||||
interpolate=True,
|
||||
encoding="utf-8",
|
||||
):
|
||||
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, bool, Optional[Text]) -> bool # noqa
|
||||
"""Parse a .env file and then load all the variables found as environment variables.
|
||||
|
||||
- *dotenv_path*: absolute or relative path to .env file.
|
||||
- *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
|
||||
- *verbose*: whether to output a warning the .env file is missing. Defaults to
|
||||
`False`.
|
||||
- *override*: whether to override the system environment variables with the variables
|
||||
in `.env` file. Defaults to `False`.
|
||||
- *encoding*: encoding to be used to read the file.
|
||||
|
||||
If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
|
||||
"""
|
||||
f = dotenv_path or stream or find_dotenv()
|
||||
dotenv = DotEnv(
|
||||
f,
|
||||
verbose=verbose,
|
||||
interpolate=interpolate,
|
||||
override=override,
|
||||
encoding=encoding,
|
||||
)
|
||||
return dotenv.set_as_environment_variables()
|
||||
|
||||
|
||||
def dotenv_values(
|
||||
dotenv_path=None,
|
||||
stream=None,
|
||||
verbose=False,
|
||||
interpolate=True,
|
||||
encoding="utf-8",
|
||||
):
|
||||
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Optional[Text]) -> Dict[Text, Optional[Text]] # noqa: E501
|
||||
"""
|
||||
Parse a .env file and return its content as a dict.
|
||||
|
||||
- *dotenv_path*: absolute or relative path to .env file.
|
||||
- *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
|
||||
- *verbose*: whether to output a warning the .env file is missing. Defaults to
|
||||
`False`.
|
||||
in `.env` file. Defaults to `False`.
|
||||
- *encoding*: encoding to be used to read the file.
|
||||
|
||||
If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
|
||||
"""
|
||||
f = dotenv_path or stream or find_dotenv()
|
||||
return DotEnv(
|
||||
f,
|
||||
verbose=verbose,
|
||||
interpolate=interpolate,
|
||||
override=True,
|
||||
encoding=encoding,
|
||||
).dict()
|
||||
@@ -1,231 +0,0 @@
|
||||
import codecs
|
||||
import re
|
||||
|
||||
from .compat import IS_TYPE_CHECKING, to_text
|
||||
|
||||
if IS_TYPE_CHECKING:
|
||||
from typing import ( # noqa:F401
|
||||
IO, Iterator, Match, NamedTuple, Optional, Pattern, Sequence, Text,
|
||||
Tuple
|
||||
)
|
||||
|
||||
|
||||
def make_regex(string, extra_flags=0):
|
||||
# type: (str, int) -> Pattern[Text]
|
||||
return re.compile(to_text(string), re.UNICODE | extra_flags)
|
||||
|
||||
|
||||
_newline = make_regex(r"(\r\n|\n|\r)")
|
||||
_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
|
||||
_whitespace = make_regex(r"[^\S\r\n]*")
|
||||
_export = make_regex(r"(?:export[^\S\r\n]+)?")
|
||||
_single_quoted_key = make_regex(r"'([^']+)'")
|
||||
_unquoted_key = make_regex(r"([^=\#\s]+)")
|
||||
_equal_sign = make_regex(r"(=[^\S\r\n]*)")
|
||||
_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
|
||||
_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
|
||||
_unquoted_value = make_regex(r"([^\r\n]*)")
|
||||
_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")
|
||||
_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
|
||||
_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
|
||||
_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
|
||||
_single_quote_escapes = make_regex(r"\\[\\']")
|
||||
|
||||
|
||||
try:
|
||||
# this is necessary because we only import these from typing
|
||||
# when we are type checking, and the linter is upset if we
|
||||
# re-import
|
||||
import typing
|
||||
|
||||
Original = typing.NamedTuple(
|
||||
"Original",
|
||||
[
|
||||
("string", typing.Text),
|
||||
("line", int),
|
||||
],
|
||||
)
|
||||
|
||||
Binding = typing.NamedTuple(
|
||||
"Binding",
|
||||
[
|
||||
("key", typing.Optional[typing.Text]),
|
||||
("value", typing.Optional[typing.Text]),
|
||||
("original", Original),
|
||||
("error", bool),
|
||||
],
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
from collections import namedtuple
|
||||
Original = namedtuple( # type: ignore
|
||||
"Original",
|
||||
[
|
||||
"string",
|
||||
"line",
|
||||
],
|
||||
)
|
||||
Binding = namedtuple( # type: ignore
|
||||
"Binding",
|
||||
[
|
||||
"key",
|
||||
"value",
|
||||
"original",
|
||||
"error",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class Position:
|
||||
def __init__(self, chars, line):
|
||||
# type: (int, int) -> None
|
||||
self.chars = chars
|
||||
self.line = line
|
||||
|
||||
@classmethod
|
||||
def start(cls):
|
||||
# type: () -> Position
|
||||
return cls(chars=0, line=1)
|
||||
|
||||
def set(self, other):
|
||||
# type: (Position) -> None
|
||||
self.chars = other.chars
|
||||
self.line = other.line
|
||||
|
||||
def advance(self, string):
|
||||
# type: (Text) -> None
|
||||
self.chars += len(string)
|
||||
self.line += len(re.findall(_newline, string))
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, stream):
|
||||
# type: (IO[Text]) -> None
|
||||
self.string = stream.read()
|
||||
self.position = Position.start()
|
||||
self.mark = Position.start()
|
||||
|
||||
def has_next(self):
|
||||
# type: () -> bool
|
||||
return self.position.chars < len(self.string)
|
||||
|
||||
def set_mark(self):
|
||||
# type: () -> None
|
||||
self.mark.set(self.position)
|
||||
|
||||
def get_marked(self):
|
||||
# type: () -> Original
|
||||
return Original(
|
||||
string=self.string[self.mark.chars:self.position.chars],
|
||||
line=self.mark.line,
|
||||
)
|
||||
|
||||
def peek(self, count):
|
||||
# type: (int) -> Text
|
||||
return self.string[self.position.chars:self.position.chars + count]
|
||||
|
||||
def read(self, count):
|
||||
# type: (int) -> Text
|
||||
result = self.string[self.position.chars:self.position.chars + count]
|
||||
if len(result) < count:
|
||||
raise Error("read: End of string")
|
||||
self.position.advance(result)
|
||||
return result
|
||||
|
||||
def read_regex(self, regex):
|
||||
# type: (Pattern[Text]) -> Sequence[Text]
|
||||
match = regex.match(self.string, self.position.chars)
|
||||
if match is None:
|
||||
raise Error("read_regex: Pattern not found")
|
||||
self.position.advance(self.string[match.start():match.end()])
|
||||
return match.groups()
|
||||
|
||||
|
||||
def decode_escapes(regex, string):
|
||||
# type: (Pattern[Text], Text) -> Text
|
||||
def decode_match(match):
|
||||
# type: (Match[Text]) -> Text
|
||||
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
|
||||
|
||||
return regex.sub(decode_match, string)
|
||||
|
||||
|
||||
def parse_key(reader):
|
||||
# type: (Reader) -> Optional[Text]
|
||||
char = reader.peek(1)
|
||||
if char == "#":
|
||||
return None
|
||||
elif char == "'":
|
||||
(key,) = reader.read_regex(_single_quoted_key)
|
||||
else:
|
||||
(key,) = reader.read_regex(_unquoted_key)
|
||||
return key
|
||||
|
||||
|
||||
def parse_unquoted_value(reader):
|
||||
# type: (Reader) -> Text
|
||||
(part,) = reader.read_regex(_unquoted_value)
|
||||
return re.sub(r"\s+#.*", "", part).rstrip()
|
||||
|
||||
|
||||
def parse_value(reader):
|
||||
# type: (Reader) -> Text
|
||||
char = reader.peek(1)
|
||||
if char == u"'":
|
||||
(value,) = reader.read_regex(_single_quoted_value)
|
||||
return decode_escapes(_single_quote_escapes, value)
|
||||
elif char == u'"':
|
||||
(value,) = reader.read_regex(_double_quoted_value)
|
||||
return decode_escapes(_double_quote_escapes, value)
|
||||
elif char in (u"", u"\n", u"\r"):
|
||||
return u""
|
||||
else:
|
||||
return parse_unquoted_value(reader)
|
||||
|
||||
|
||||
def parse_binding(reader):
|
||||
# type: (Reader) -> Binding
|
||||
reader.set_mark()
|
||||
try:
|
||||
reader.read_regex(_multiline_whitespace)
|
||||
if not reader.has_next():
|
||||
return Binding(
|
||||
key=None,
|
||||
value=None,
|
||||
original=reader.get_marked(),
|
||||
error=False,
|
||||
)
|
||||
reader.read_regex(_export)
|
||||
key = parse_key(reader)
|
||||
reader.read_regex(_whitespace)
|
||||
if reader.peek(1) == "=":
|
||||
reader.read_regex(_equal_sign)
|
||||
value = parse_value(reader) # type: Optional[Text]
|
||||
else:
|
||||
value = None
|
||||
reader.read_regex(_comment)
|
||||
reader.read_regex(_end_of_line)
|
||||
return Binding(
|
||||
key=key,
|
||||
value=value,
|
||||
original=reader.get_marked(),
|
||||
error=False,
|
||||
)
|
||||
except Error:
|
||||
reader.read_regex(_rest_of_line)
|
||||
return Binding(
|
||||
key=None,
|
||||
value=None,
|
||||
original=reader.get_marked(),
|
||||
error=True,
|
||||
)
|
||||
|
||||
|
||||
def parse_stream(stream):
|
||||
# type: (IO[Text]) -> Iterator[Binding]
|
||||
reader = Reader(stream)
|
||||
while reader.has_next():
|
||||
yield parse_binding(reader)
|
||||
@@ -1 +0,0 @@
|
||||
# Marker file for PEP 561
|
||||
@@ -1,106 +0,0 @@
|
||||
import re
|
||||
from abc import ABCMeta
|
||||
|
||||
from .compat import IS_TYPE_CHECKING
|
||||
|
||||
if IS_TYPE_CHECKING:
|
||||
from typing import Iterator, Mapping, Optional, Pattern, Text
|
||||
|
||||
|
||||
_posix_variable = re.compile(
|
||||
r"""
|
||||
\$\{
|
||||
(?P<name>[^\}:]*)
|
||||
(?::-
|
||||
(?P<default>[^\}]*)
|
||||
)?
|
||||
\}
|
||||
""",
|
||||
re.VERBOSE,
|
||||
) # type: Pattern[Text]
|
||||
|
||||
|
||||
class Atom():
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __ne__(self, other):
|
||||
# type: (object) -> bool
|
||||
result = self.__eq__(other)
|
||||
if result is NotImplemented:
|
||||
return NotImplemented
|
||||
return not result
|
||||
|
||||
def resolve(self, env):
|
||||
# type: (Mapping[Text, Optional[Text]]) -> Text
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Literal(Atom):
|
||||
def __init__(self, value):
|
||||
# type: (Text) -> None
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "Literal(value={})".format(self.value)
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (object) -> bool
|
||||
if not isinstance(other, self.__class__):
|
||||
return NotImplemented
|
||||
return self.value == other.value
|
||||
|
||||
def __hash__(self):
|
||||
# type: () -> int
|
||||
return hash((self.__class__, self.value))
|
||||
|
||||
def resolve(self, env):
|
||||
# type: (Mapping[Text, Optional[Text]]) -> Text
|
||||
return self.value
|
||||
|
||||
|
||||
class Variable(Atom):
|
||||
def __init__(self, name, default):
|
||||
# type: (Text, Optional[Text]) -> None
|
||||
self.name = name
|
||||
self.default = default
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "Variable(name={}, default={})".format(self.name, self.default)
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (object) -> bool
|
||||
if not isinstance(other, self.__class__):
|
||||
return NotImplemented
|
||||
return (self.name, self.default) == (other.name, other.default)
|
||||
|
||||
def __hash__(self):
|
||||
# type: () -> int
|
||||
return hash((self.__class__, self.name, self.default))
|
||||
|
||||
def resolve(self, env):
|
||||
# type: (Mapping[Text, Optional[Text]]) -> Text
|
||||
default = self.default if self.default is not None else ""
|
||||
result = env.get(self.name, default)
|
||||
return result if result is not None else ""
|
||||
|
||||
|
||||
def parse_variables(value):
|
||||
# type: (Text) -> Iterator[Atom]
|
||||
cursor = 0
|
||||
|
||||
for match in _posix_variable.finditer(value):
|
||||
(start, end) = match.span()
|
||||
name = match.groupdict()["name"]
|
||||
default = match.groupdict()["default"]
|
||||
|
||||
if start > cursor:
|
||||
yield Literal(value=value[cursor:start])
|
||||
|
||||
yield Variable(name=name, default=default)
|
||||
cursor = end
|
||||
|
||||
length = len(value)
|
||||
if cursor < length:
|
||||
yield Literal(value=value[cursor:length])
|
||||
@@ -1 +0,0 @@
|
||||
__version__ = "0.17.1"
|
||||
@@ -23,7 +23,7 @@ import ast
|
||||
import textwrap
|
||||
import sys
|
||||
|
||||
from pyboard import PyboardError
|
||||
from ampy.pyboard import PyboardError
|
||||
|
||||
|
||||
BUFFER_SIZE = 32 # Amount of data to read or write to the serial port at a time.
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a wrapper module for different platform implementations
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
from serial.serialutil import *
|
||||
#~ SerialBase, SerialException, to_bytes, iterbytes
|
||||
|
||||
__version__ = '3.5'
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
if sys.platform == 'cli':
|
||||
from serial.serialcli import Serial
|
||||
else:
|
||||
import os
|
||||
# chose an implementation, depending on os
|
||||
if os.name == 'nt': # sys.platform == 'win32':
|
||||
from serial.serialwin32 import Serial
|
||||
elif os.name == 'posix':
|
||||
from serial.serialposix import Serial, PosixPollSerial, VTIMESerial # noqa
|
||||
elif os.name == 'java':
|
||||
from serial.serialjava import Serial
|
||||
else:
|
||||
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||
|
||||
|
||||
protocol_handler_packages = [
|
||||
'serial.urlhandler',
|
||||
]
|
||||
|
||||
|
||||
def serial_for_url(url, *args, **kwargs):
|
||||
"""\
|
||||
Get an instance of the Serial class, depending on port/url. The port is not
|
||||
opened when the keyword parameter 'do_not_open' is true, by default it
|
||||
is. All other parameters are directly passed to the __init__ method when
|
||||
the port is instantiated.
|
||||
|
||||
The list of package names that is searched for protocol handlers is kept in
|
||||
``protocol_handler_packages``.
|
||||
|
||||
e.g. we want to support a URL ``foobar://``. A module
|
||||
``my_handlers.protocol_foobar`` is provided by the user. Then
|
||||
``protocol_handler_packages.append("my_handlers")`` would extend the search
|
||||
path so that ``serial_for_url("foobar://"))`` would work.
|
||||
"""
|
||||
# check and remove extra parameter to not confuse the Serial class
|
||||
do_open = not kwargs.pop('do_not_open', False)
|
||||
# the default is to use the native implementation
|
||||
klass = Serial
|
||||
try:
|
||||
url_lowercase = url.lower()
|
||||
except AttributeError:
|
||||
# it's not a string, use default
|
||||
pass
|
||||
else:
|
||||
# if it is an URL, try to import the handler module from the list of possible packages
|
||||
if '://' in url_lowercase:
|
||||
protocol = url_lowercase.split('://', 1)[0]
|
||||
module_name = '.protocol_{}'.format(protocol)
|
||||
for package_name in protocol_handler_packages:
|
||||
try:
|
||||
importlib.import_module(package_name)
|
||||
handler_module = importlib.import_module(module_name, package_name)
|
||||
except ImportError:
|
||||
continue
|
||||
else:
|
||||
if hasattr(handler_module, 'serial_class_for_url'):
|
||||
url, klass = handler_module.serial_class_for_url(url)
|
||||
else:
|
||||
klass = handler_module.Serial
|
||||
break
|
||||
else:
|
||||
raise ValueError('invalid URL, protocol {!r} not known'.format(protocol))
|
||||
# instantiate and open when desired
|
||||
instance = klass(None, *args, **kwargs)
|
||||
instance.port = url
|
||||
if do_open:
|
||||
instance.open()
|
||||
return instance
|
||||
@@ -1,3 +0,0 @@
|
||||
from .tools import miniterm
|
||||
|
||||
miniterm.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# RS485 support
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""\
|
||||
The settings for RS485 are stored in a dedicated object that can be applied to
|
||||
serial ports (where supported).
|
||||
NOTE: Some implementations may only support a subset of the settings.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import time
|
||||
import serial
|
||||
|
||||
|
||||
class RS485Settings(object):
|
||||
def __init__(
|
||||
self,
|
||||
rts_level_for_tx=True,
|
||||
rts_level_for_rx=False,
|
||||
loopback=False,
|
||||
delay_before_tx=None,
|
||||
delay_before_rx=None):
|
||||
self.rts_level_for_tx = rts_level_for_tx
|
||||
self.rts_level_for_rx = rts_level_for_rx
|
||||
self.loopback = loopback
|
||||
self.delay_before_tx = delay_before_tx
|
||||
self.delay_before_rx = delay_before_rx
|
||||
|
||||
|
||||
class RS485(serial.Serial):
|
||||
"""\
|
||||
A subclass that replaces the write method with one that toggles RTS
|
||||
according to the RS485 settings.
|
||||
|
||||
NOTE: This may work unreliably on some serial ports (control signals not
|
||||
synchronized or delayed compared to data). Using delays may be
|
||||
unreliable (varying times, larger than expected) as the OS may not
|
||||
support very fine grained delays (no smaller than in the order of
|
||||
tens of milliseconds).
|
||||
|
||||
NOTE: Some implementations support this natively. Better performance
|
||||
can be expected when the native version is used.
|
||||
|
||||
NOTE: The loopback property is ignored by this implementation. The actual
|
||||
behavior depends on the used hardware.
|
||||
|
||||
Usage:
|
||||
|
||||
ser = RS485(...)
|
||||
ser.rs485_mode = RS485Settings(...)
|
||||
ser.write(b'hello')
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RS485, self).__init__(*args, **kwargs)
|
||||
self._alternate_rs485_settings = None
|
||||
|
||||
def write(self, b):
|
||||
"""Write to port, controlling RTS before and after transmitting."""
|
||||
if self._alternate_rs485_settings is not None:
|
||||
# apply level for TX and optional delay
|
||||
self.setRTS(self._alternate_rs485_settings.rts_level_for_tx)
|
||||
if self._alternate_rs485_settings.delay_before_tx is not None:
|
||||
time.sleep(self._alternate_rs485_settings.delay_before_tx)
|
||||
# write and wait for data to be written
|
||||
super(RS485, self).write(b)
|
||||
super(RS485, self).flush()
|
||||
# optional delay and apply level for RX
|
||||
if self._alternate_rs485_settings.delay_before_rx is not None:
|
||||
time.sleep(self._alternate_rs485_settings.delay_before_rx)
|
||||
self.setRTS(self._alternate_rs485_settings.rts_level_for_rx)
|
||||
else:
|
||||
super(RS485, self).write(b)
|
||||
|
||||
# redirect where the property stores the settings so that underlying Serial
|
||||
# instance does not see them
|
||||
@property
|
||||
def rs485_mode(self):
|
||||
"""\
|
||||
Enable RS485 mode and apply new settings, set to None to disable.
|
||||
See serial.rs485.RS485Settings for more info about the value.
|
||||
"""
|
||||
return self._alternate_rs485_settings
|
||||
|
||||
@rs485_mode.setter
|
||||
def rs485_mode(self, rs485_settings):
|
||||
self._alternate_rs485_settings = rs485_settings
|
||||
@@ -1,253 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Backend for .NET/Mono (IronPython), .NET >= 2
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2008-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import System
|
||||
import System.IO.Ports
|
||||
from serial.serialutil import *
|
||||
|
||||
# must invoke function with byte array, make a helper to convert strings
|
||||
# to byte arrays
|
||||
sab = System.Array[System.Byte]
|
||||
|
||||
|
||||
def as_byte_array(string):
|
||||
return sab([ord(x) for x in string]) # XXX will require adaption when run with a 3.x compatible IronPython
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""Serial port implementation for .NET/Mono."""
|
||||
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200)
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
try:
|
||||
self._port_handle = System.IO.Ports.SerialPort(self.portstr)
|
||||
except Exception as msg:
|
||||
self._port_handle = None
|
||||
raise SerialException("could not open port %s: %s" % (self.portstr, msg))
|
||||
|
||||
# if RTS and/or DTR are not set before open, they default to True
|
||||
if self._rts_state is None:
|
||||
self._rts_state = True
|
||||
if self._dtr_state is None:
|
||||
self._dtr_state = True
|
||||
|
||||
self._reconfigure_port()
|
||||
self._port_handle.Open()
|
||||
self.is_open = True
|
||||
if not self._dsrdtr:
|
||||
self._update_dtr_state()
|
||||
if not self._rtscts:
|
||||
self._update_rts_state()
|
||||
self.reset_input_buffer()
|
||||
|
||||
def _reconfigure_port(self):
|
||||
"""Set communication parameters on opened port."""
|
||||
if not self._port_handle:
|
||||
raise SerialException("Can only operate on a valid port handle")
|
||||
|
||||
#~ self._port_handle.ReceivedBytesThreshold = 1
|
||||
|
||||
if self._timeout is None:
|
||||
self._port_handle.ReadTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
|
||||
else:
|
||||
self._port_handle.ReadTimeout = int(self._timeout * 1000)
|
||||
|
||||
# if self._timeout != 0 and self._interCharTimeout is not None:
|
||||
# timeouts = (int(self._interCharTimeout * 1000),) + timeouts[1:]
|
||||
|
||||
if self._write_timeout is None:
|
||||
self._port_handle.WriteTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
|
||||
else:
|
||||
self._port_handle.WriteTimeout = int(self._write_timeout * 1000)
|
||||
|
||||
# Setup the connection info.
|
||||
try:
|
||||
self._port_handle.BaudRate = self._baudrate
|
||||
except IOError as e:
|
||||
# catch errors from illegal baudrate settings
|
||||
raise ValueError(str(e))
|
||||
|
||||
if self._bytesize == FIVEBITS:
|
||||
self._port_handle.DataBits = 5
|
||||
elif self._bytesize == SIXBITS:
|
||||
self._port_handle.DataBits = 6
|
||||
elif self._bytesize == SEVENBITS:
|
||||
self._port_handle.DataBits = 7
|
||||
elif self._bytesize == EIGHTBITS:
|
||||
self._port_handle.DataBits = 8
|
||||
else:
|
||||
raise ValueError("Unsupported number of data bits: %r" % self._bytesize)
|
||||
|
||||
if self._parity == PARITY_NONE:
|
||||
self._port_handle.Parity = getattr(System.IO.Ports.Parity, 'None') # reserved keyword in Py3k
|
||||
elif self._parity == PARITY_EVEN:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Even
|
||||
elif self._parity == PARITY_ODD:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Odd
|
||||
elif self._parity == PARITY_MARK:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Mark
|
||||
elif self._parity == PARITY_SPACE:
|
||||
self._port_handle.Parity = System.IO.Ports.Parity.Space
|
||||
else:
|
||||
raise ValueError("Unsupported parity mode: %r" % self._parity)
|
||||
|
||||
if self._stopbits == STOPBITS_ONE:
|
||||
self._port_handle.StopBits = System.IO.Ports.StopBits.One
|
||||
elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
|
||||
self._port_handle.StopBits = System.IO.Ports.StopBits.OnePointFive
|
||||
elif self._stopbits == STOPBITS_TWO:
|
||||
self._port_handle.StopBits = System.IO.Ports.StopBits.Two
|
||||
else:
|
||||
raise ValueError("Unsupported number of stop bits: %r" % self._stopbits)
|
||||
|
||||
if self._rtscts and self._xonxoff:
|
||||
self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSendXOnXOff
|
||||
elif self._rtscts:
|
||||
self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSend
|
||||
elif self._xonxoff:
|
||||
self._port_handle.Handshake = System.IO.Ports.Handshake.XOnXOff
|
||||
else:
|
||||
self._port_handle.Handshake = getattr(System.IO.Ports.Handshake, 'None') # reserved keyword in Py3k
|
||||
|
||||
#~ def __del__(self):
|
||||
#~ self.close()
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
if self._port_handle:
|
||||
try:
|
||||
self._port_handle.Close()
|
||||
except System.IO.Ports.InvalidOperationException:
|
||||
# ignore errors. can happen for unplugged USB serial devices
|
||||
pass
|
||||
self._port_handle = None
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of characters currently in the input buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.BytesToRead
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
# must use single byte reads as this is the only way to read
|
||||
# without applying encodings
|
||||
data = bytearray()
|
||||
while size:
|
||||
try:
|
||||
data.append(self._port_handle.ReadByte())
|
||||
except System.TimeoutException:
|
||||
break
|
||||
else:
|
||||
size -= 1
|
||||
return bytes(data)
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given string over the serial port."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
#~ if not isinstance(data, (bytes, bytearray)):
|
||||
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||
try:
|
||||
# must call overloaded method with byte array argument
|
||||
# as this is the only one not applying encodings
|
||||
self._port_handle.Write(as_byte_array(data), 0, len(data))
|
||||
except System.TimeoutException:
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
return len(data)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.DiscardInBuffer()
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.DiscardOutBuffer()
|
||||
|
||||
def _update_break_state(self):
|
||||
"""
|
||||
Set break: Controls TXD. When active, to transmitting is possible.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.BreakState = bool(self._break_state)
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.RtsEnable = bool(self._rts_state)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._port_handle.DtrEnable = bool(self._dtr_state)
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.CtsHolding
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.DsrHolding
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
#~ return self._port_handle.XXX
|
||||
return False # XXX an error would be better
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self._port_handle.CDHolding
|
||||
|
||||
# - - platform specific - - - -
|
||||
# none
|
||||
@@ -1,251 +0,0 @@
|
||||
#!jython
|
||||
#
|
||||
# Backend Jython with JavaComm
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2002-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from serial.serialutil import *
|
||||
|
||||
|
||||
def my_import(name):
|
||||
mod = __import__(name)
|
||||
components = name.split('.')
|
||||
for comp in components[1:]:
|
||||
mod = getattr(mod, comp)
|
||||
return mod
|
||||
|
||||
|
||||
def detect_java_comm(names):
|
||||
"""try given list of modules and return that imports"""
|
||||
for name in names:
|
||||
try:
|
||||
mod = my_import(name)
|
||||
mod.SerialPort
|
||||
return mod
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
raise ImportError("No Java Communications API implementation found")
|
||||
|
||||
|
||||
# Java Communications API implementations
|
||||
# http://mho.republika.pl/java/comm/
|
||||
|
||||
comm = detect_java_comm([
|
||||
'javax.comm', # Sun/IBM
|
||||
'gnu.io', # RXTX
|
||||
])
|
||||
|
||||
|
||||
def device(portnumber):
|
||||
"""Turn a port number into a device name"""
|
||||
enum = comm.CommPortIdentifier.getPortIdentifiers()
|
||||
ports = []
|
||||
while enum.hasMoreElements():
|
||||
el = enum.nextElement()
|
||||
if el.getPortType() == comm.CommPortIdentifier.PORT_SERIAL:
|
||||
ports.append(el)
|
||||
return ports[portnumber].getName()
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""\
|
||||
Serial port class, implemented with Java Communications API and
|
||||
thus usable with jython and the appropriate java extension.
|
||||
"""
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
if type(self._port) == type(''): # strings are taken directly
|
||||
portId = comm.CommPortIdentifier.getPortIdentifier(self._port)
|
||||
else:
|
||||
portId = comm.CommPortIdentifier.getPortIdentifier(device(self._port)) # numbers are transformed to a comport id obj
|
||||
try:
|
||||
self.sPort = portId.open("python serial module", 10)
|
||||
except Exception as msg:
|
||||
self.sPort = None
|
||||
raise SerialException("Could not open port: %s" % msg)
|
||||
self._reconfigurePort()
|
||||
self._instream = self.sPort.getInputStream()
|
||||
self._outstream = self.sPort.getOutputStream()
|
||||
self.is_open = True
|
||||
|
||||
def _reconfigurePort(self):
|
||||
"""Set communication parameters on opened port."""
|
||||
if not self.sPort:
|
||||
raise SerialException("Can only operate on a valid port handle")
|
||||
|
||||
self.sPort.enableReceiveTimeout(30)
|
||||
if self._bytesize == FIVEBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_5
|
||||
elif self._bytesize == SIXBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_6
|
||||
elif self._bytesize == SEVENBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_7
|
||||
elif self._bytesize == EIGHTBITS:
|
||||
jdatabits = comm.SerialPort.DATABITS_8
|
||||
else:
|
||||
raise ValueError("unsupported bytesize: %r" % self._bytesize)
|
||||
|
||||
if self._stopbits == STOPBITS_ONE:
|
||||
jstopbits = comm.SerialPort.STOPBITS_1
|
||||
elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
|
||||
jstopbits = comm.SerialPort.STOPBITS_1_5
|
||||
elif self._stopbits == STOPBITS_TWO:
|
||||
jstopbits = comm.SerialPort.STOPBITS_2
|
||||
else:
|
||||
raise ValueError("unsupported number of stopbits: %r" % self._stopbits)
|
||||
|
||||
if self._parity == PARITY_NONE:
|
||||
jparity = comm.SerialPort.PARITY_NONE
|
||||
elif self._parity == PARITY_EVEN:
|
||||
jparity = comm.SerialPort.PARITY_EVEN
|
||||
elif self._parity == PARITY_ODD:
|
||||
jparity = comm.SerialPort.PARITY_ODD
|
||||
elif self._parity == PARITY_MARK:
|
||||
jparity = comm.SerialPort.PARITY_MARK
|
||||
elif self._parity == PARITY_SPACE:
|
||||
jparity = comm.SerialPort.PARITY_SPACE
|
||||
else:
|
||||
raise ValueError("unsupported parity type: %r" % self._parity)
|
||||
|
||||
jflowin = jflowout = 0
|
||||
if self._rtscts:
|
||||
jflowin |= comm.SerialPort.FLOWCONTROL_RTSCTS_IN
|
||||
jflowout |= comm.SerialPort.FLOWCONTROL_RTSCTS_OUT
|
||||
if self._xonxoff:
|
||||
jflowin |= comm.SerialPort.FLOWCONTROL_XONXOFF_IN
|
||||
jflowout |= comm.SerialPort.FLOWCONTROL_XONXOFF_OUT
|
||||
|
||||
self.sPort.setSerialPortParams(self._baudrate, jdatabits, jstopbits, jparity)
|
||||
self.sPort.setFlowControlMode(jflowin | jflowout)
|
||||
|
||||
if self._timeout >= 0:
|
||||
self.sPort.enableReceiveTimeout(int(self._timeout*1000))
|
||||
else:
|
||||
self.sPort.disableReceiveTimeout()
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
if self.sPort:
|
||||
self._instream.close()
|
||||
self._outstream.close()
|
||||
self.sPort.close()
|
||||
self.sPort = None
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of characters currently in the input buffer."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
return self._instream.available()
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
if size > 0:
|
||||
while len(read) < size:
|
||||
x = self._instream.read()
|
||||
if x == -1:
|
||||
if self.timeout >= 0:
|
||||
break
|
||||
else:
|
||||
read.append(x)
|
||||
return bytes(read)
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given string over the serial port."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||
self._outstream.write(data)
|
||||
return len(data)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self._instream.skip(self._instream.available())
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer.
|
||||
"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self._outstream.flush()
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""Send break condition. Timed, returns to idle state after given duration."""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.sendBreak(duration*1000.0)
|
||||
|
||||
def _update_break_state(self):
|
||||
"""Set break: Controls TXD. When active, to transmitting is possible."""
|
||||
if self.fd is None:
|
||||
raise PortNotOpenError()
|
||||
raise SerialException("The _update_break_state function is not implemented in java.")
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.setRTS(self._rts_state)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.setDTR(self._dtr_state)
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isCTS()
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isDSR()
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isRI()
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.sPort:
|
||||
raise PortNotOpenError()
|
||||
self.sPort.isCD()
|
||||
@@ -1,900 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# backend for serial IO for POSIX compatible systems, like Linux, OSX
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# parts based on code from Grant B. Edwards <grante@visi.com>:
|
||||
# ftp://ftp.visi.com/users/grante/python/PosixSerial.py
|
||||
#
|
||||
# references: http://www.easysw.com/~mike/serial/serial.html
|
||||
|
||||
# Collection of port names (was previously used by number_to_device which was
|
||||
# removed.
|
||||
# - Linux /dev/ttyS%d (confirmed)
|
||||
# - cygwin/win32 /dev/com%d (confirmed)
|
||||
# - openbsd (OpenBSD) /dev/cua%02d
|
||||
# - bsd*, freebsd* /dev/cuad%d
|
||||
# - darwin (OS X) /dev/cuad%d
|
||||
# - netbsd /dev/dty%02d (NetBSD 1.6 testing by Erk)
|
||||
# - irix (IRIX) /dev/ttyf%d (partially tested) names depending on flow control
|
||||
# - hp (HP-UX) /dev/tty%dp0 (not tested)
|
||||
# - sunos (Solaris/SunOS) /dev/tty%c (letters, 'a'..'z') (confirmed)
|
||||
# - aix (AIX) /dev/tty%d
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
import errno
|
||||
import fcntl
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
|
||||
import serial
|
||||
from serial.serialutil import SerialBase, SerialException, to_bytes, \
|
||||
PortNotOpenError, SerialTimeoutException, Timeout
|
||||
|
||||
|
||||
class PlatformSpecificBase(object):
|
||||
BAUDRATE_CONSTANTS = {}
|
||||
|
||||
def _set_special_baudrate(self, baudrate):
|
||||
raise NotImplementedError('non-standard baudrates are not supported on this platform')
|
||||
|
||||
def _set_rs485_mode(self, rs485_settings):
|
||||
raise NotImplementedError('RS485 not supported on this platform')
|
||||
|
||||
def set_low_latency_mode(self, low_latency_settings):
|
||||
raise NotImplementedError('Low latency not supported on this platform')
|
||||
|
||||
def _update_break_state(self):
|
||||
"""\
|
||||
Set break: Controls TXD. When active, no transmitting is possible.
|
||||
"""
|
||||
if self._break_state:
|
||||
fcntl.ioctl(self.fd, TIOCSBRK)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, TIOCCBRK)
|
||||
|
||||
|
||||
# some systems support an extra flag to enable the two in POSIX unsupported
|
||||
# paritiy settings for MARK and SPACE
|
||||
CMSPAR = 0 # default, for unsupported platforms, override below
|
||||
|
||||
# try to detect the OS so that a device can be selected...
|
||||
# this code block should supply a device() and set_special_baudrate() function
|
||||
# for the platform
|
||||
plat = sys.platform.lower()
|
||||
|
||||
if plat[:5] == 'linux': # Linux (confirmed) # noqa
|
||||
import array
|
||||
|
||||
# extra termios flags
|
||||
CMSPAR = 0o10000000000 # Use "stick" (mark/space) parity
|
||||
|
||||
# baudrate ioctls
|
||||
TCGETS2 = 0x802C542A
|
||||
TCSETS2 = 0x402C542B
|
||||
BOTHER = 0o010000
|
||||
|
||||
# RS485 ioctls
|
||||
TIOCGRS485 = 0x542E
|
||||
TIOCSRS485 = 0x542F
|
||||
SER_RS485_ENABLED = 0b00000001
|
||||
SER_RS485_RTS_ON_SEND = 0b00000010
|
||||
SER_RS485_RTS_AFTER_SEND = 0b00000100
|
||||
SER_RS485_RX_DURING_TX = 0b00010000
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
BAUDRATE_CONSTANTS = {
|
||||
0: 0o000000, # hang up
|
||||
50: 0o000001,
|
||||
75: 0o000002,
|
||||
110: 0o000003,
|
||||
134: 0o000004,
|
||||
150: 0o000005,
|
||||
200: 0o000006,
|
||||
300: 0o000007,
|
||||
600: 0o000010,
|
||||
1200: 0o000011,
|
||||
1800: 0o000012,
|
||||
2400: 0o000013,
|
||||
4800: 0o000014,
|
||||
9600: 0o000015,
|
||||
19200: 0o000016,
|
||||
38400: 0o000017,
|
||||
57600: 0o010001,
|
||||
115200: 0o010002,
|
||||
230400: 0o010003,
|
||||
460800: 0o010004,
|
||||
500000: 0o010005,
|
||||
576000: 0o010006,
|
||||
921600: 0o010007,
|
||||
1000000: 0o010010,
|
||||
1152000: 0o010011,
|
||||
1500000: 0o010012,
|
||||
2000000: 0o010013,
|
||||
2500000: 0o010014,
|
||||
3000000: 0o010015,
|
||||
3500000: 0o010016,
|
||||
4000000: 0o010017
|
||||
}
|
||||
|
||||
def set_low_latency_mode(self, low_latency_settings):
|
||||
buf = array.array('i', [0] * 32)
|
||||
|
||||
try:
|
||||
# get serial_struct
|
||||
fcntl.ioctl(self.fd, termios.TIOCGSERIAL, buf)
|
||||
|
||||
# set or unset ASYNC_LOW_LATENCY flag
|
||||
if low_latency_settings:
|
||||
buf[4] |= 0x2000
|
||||
else:
|
||||
buf[4] &= ~0x2000
|
||||
|
||||
# set serial_struct
|
||||
fcntl.ioctl(self.fd, termios.TIOCSSERIAL, buf)
|
||||
except IOError as e:
|
||||
raise ValueError('Failed to update ASYNC_LOW_LATENCY flag to {}: {}'.format(low_latency_settings, e))
|
||||
|
||||
def _set_special_baudrate(self, baudrate):
|
||||
# right size is 44 on x86_64, allow for some growth
|
||||
buf = array.array('i', [0] * 64)
|
||||
try:
|
||||
# get serial_struct
|
||||
fcntl.ioctl(self.fd, TCGETS2, buf)
|
||||
# set custom speed
|
||||
buf[2] &= ~termios.CBAUD
|
||||
buf[2] |= BOTHER
|
||||
buf[9] = buf[10] = baudrate
|
||||
|
||||
# set serial_struct
|
||||
fcntl.ioctl(self.fd, TCSETS2, buf)
|
||||
except IOError as e:
|
||||
raise ValueError('Failed to set custom baud rate ({}): {}'.format(baudrate, e))
|
||||
|
||||
def _set_rs485_mode(self, rs485_settings):
|
||||
buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
|
||||
try:
|
||||
fcntl.ioctl(self.fd, TIOCGRS485, buf)
|
||||
buf[0] |= SER_RS485_ENABLED
|
||||
if rs485_settings is not None:
|
||||
if rs485_settings.loopback:
|
||||
buf[0] |= SER_RS485_RX_DURING_TX
|
||||
else:
|
||||
buf[0] &= ~SER_RS485_RX_DURING_TX
|
||||
if rs485_settings.rts_level_for_tx:
|
||||
buf[0] |= SER_RS485_RTS_ON_SEND
|
||||
else:
|
||||
buf[0] &= ~SER_RS485_RTS_ON_SEND
|
||||
if rs485_settings.rts_level_for_rx:
|
||||
buf[0] |= SER_RS485_RTS_AFTER_SEND
|
||||
else:
|
||||
buf[0] &= ~SER_RS485_RTS_AFTER_SEND
|
||||
if rs485_settings.delay_before_tx is not None:
|
||||
buf[1] = int(rs485_settings.delay_before_tx * 1000)
|
||||
if rs485_settings.delay_before_rx is not None:
|
||||
buf[2] = int(rs485_settings.delay_before_rx * 1000)
|
||||
else:
|
||||
buf[0] = 0 # clear SER_RS485_ENABLED
|
||||
fcntl.ioctl(self.fd, TIOCSRS485, buf)
|
||||
except IOError as e:
|
||||
raise ValueError('Failed to set RS485 mode: {}'.format(e))
|
||||
|
||||
|
||||
elif plat == 'cygwin': # cygwin/win32 (confirmed)
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
BAUDRATE_CONSTANTS = {
|
||||
128000: 0x01003,
|
||||
256000: 0x01005,
|
||||
500000: 0x01007,
|
||||
576000: 0x01008,
|
||||
921600: 0x01009,
|
||||
1000000: 0x0100a,
|
||||
1152000: 0x0100b,
|
||||
1500000: 0x0100c,
|
||||
2000000: 0x0100d,
|
||||
2500000: 0x0100e,
|
||||
3000000: 0x0100f
|
||||
}
|
||||
|
||||
|
||||
elif plat[:6] == 'darwin': # OS X
|
||||
import array
|
||||
IOSSIOSPEED = 0x80045402 # _IOW('T', 2, speed_t)
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
osx_version = os.uname()[2].split('.')
|
||||
TIOCSBRK = 0x2000747B # _IO('t', 123)
|
||||
TIOCCBRK = 0x2000747A # _IO('t', 122)
|
||||
|
||||
# Tiger or above can support arbitrary serial speeds
|
||||
if int(osx_version[0]) >= 8:
|
||||
def _set_special_baudrate(self, baudrate):
|
||||
# use IOKit-specific call to set up high speeds
|
||||
buf = array.array('i', [baudrate])
|
||||
fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1)
|
||||
|
||||
def _update_break_state(self):
|
||||
"""\
|
||||
Set break: Controls TXD. When active, no transmitting is possible.
|
||||
"""
|
||||
if self._break_state:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
|
||||
|
||||
elif plat[:3] == 'bsd' or \
|
||||
plat[:7] == 'freebsd' or \
|
||||
plat[:6] == 'netbsd' or \
|
||||
plat[:7] == 'openbsd':
|
||||
|
||||
class ReturnBaudrate(object):
|
||||
def __getitem__(self, key):
|
||||
return key
|
||||
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
# Only tested on FreeBSD:
|
||||
# The baud rate may be passed in as
|
||||
# a literal value.
|
||||
BAUDRATE_CONSTANTS = ReturnBaudrate()
|
||||
|
||||
TIOCSBRK = 0x2000747B # _IO('t', 123)
|
||||
TIOCCBRK = 0x2000747A # _IO('t', 122)
|
||||
|
||||
|
||||
def _update_break_state(self):
|
||||
"""\
|
||||
Set break: Controls TXD. When active, no transmitting is possible.
|
||||
"""
|
||||
if self._break_state:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
|
||||
|
||||
else:
|
||||
class PlatformSpecific(PlatformSpecificBase):
|
||||
pass
|
||||
|
||||
|
||||
# load some constants for later use.
|
||||
# try to use values from termios, use defaults from linux otherwise
|
||||
TIOCMGET = getattr(termios, 'TIOCMGET', 0x5415)
|
||||
TIOCMBIS = getattr(termios, 'TIOCMBIS', 0x5416)
|
||||
TIOCMBIC = getattr(termios, 'TIOCMBIC', 0x5417)
|
||||
TIOCMSET = getattr(termios, 'TIOCMSET', 0x5418)
|
||||
|
||||
# TIOCM_LE = getattr(termios, 'TIOCM_LE', 0x001)
|
||||
TIOCM_DTR = getattr(termios, 'TIOCM_DTR', 0x002)
|
||||
TIOCM_RTS = getattr(termios, 'TIOCM_RTS', 0x004)
|
||||
# TIOCM_ST = getattr(termios, 'TIOCM_ST', 0x008)
|
||||
# TIOCM_SR = getattr(termios, 'TIOCM_SR', 0x010)
|
||||
|
||||
TIOCM_CTS = getattr(termios, 'TIOCM_CTS', 0x020)
|
||||
TIOCM_CAR = getattr(termios, 'TIOCM_CAR', 0x040)
|
||||
TIOCM_RNG = getattr(termios, 'TIOCM_RNG', 0x080)
|
||||
TIOCM_DSR = getattr(termios, 'TIOCM_DSR', 0x100)
|
||||
TIOCM_CD = getattr(termios, 'TIOCM_CD', TIOCM_CAR)
|
||||
TIOCM_RI = getattr(termios, 'TIOCM_RI', TIOCM_RNG)
|
||||
# TIOCM_OUT1 = getattr(termios, 'TIOCM_OUT1', 0x2000)
|
||||
# TIOCM_OUT2 = getattr(termios, 'TIOCM_OUT2', 0x4000)
|
||||
if hasattr(termios, 'TIOCINQ'):
|
||||
TIOCINQ = termios.TIOCINQ
|
||||
else:
|
||||
TIOCINQ = getattr(termios, 'FIONREAD', 0x541B)
|
||||
TIOCOUTQ = getattr(termios, 'TIOCOUTQ', 0x5411)
|
||||
|
||||
TIOCM_zero_str = struct.pack('I', 0)
|
||||
TIOCM_RTS_str = struct.pack('I', TIOCM_RTS)
|
||||
TIOCM_DTR_str = struct.pack('I', TIOCM_DTR)
|
||||
|
||||
TIOCSBRK = getattr(termios, 'TIOCSBRK', 0x5427)
|
||||
TIOCCBRK = getattr(termios, 'TIOCCBRK', 0x5428)
|
||||
|
||||
|
||||
class Serial(SerialBase, PlatformSpecific):
|
||||
"""\
|
||||
Serial port class POSIX implementation. Serial port configuration is
|
||||
done with termios and fcntl. Runs on Linux and many other Un*x like
|
||||
systems.
|
||||
"""
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened."""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
self.fd = None
|
||||
# open
|
||||
try:
|
||||
self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
||||
except OSError as msg:
|
||||
self.fd = None
|
||||
raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
|
||||
#~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # set blocking
|
||||
|
||||
self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
|
||||
self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
|
||||
|
||||
try:
|
||||
self._reconfigure_port(force_update=True)
|
||||
|
||||
try:
|
||||
if not self._dsrdtr:
|
||||
self._update_dtr_state()
|
||||
if not self._rtscts:
|
||||
self._update_rts_state()
|
||||
except IOError as e:
|
||||
# ignore Invalid argument and Inappropriate ioctl
|
||||
if e.errno not in (errno.EINVAL, errno.ENOTTY):
|
||||
raise
|
||||
|
||||
self._reset_input_buffer()
|
||||
|
||||
self.pipe_abort_read_r, self.pipe_abort_read_w = os.pipe()
|
||||
self.pipe_abort_write_r, self.pipe_abort_write_w = os.pipe()
|
||||
fcntl.fcntl(self.pipe_abort_read_r, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
fcntl.fcntl(self.pipe_abort_write_r, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
except BaseException:
|
||||
try:
|
||||
os.close(self.fd)
|
||||
except Exception:
|
||||
# ignore any exception when closing the port
|
||||
# also to keep original exception that happened when setting up
|
||||
pass
|
||||
self.fd = None
|
||||
|
||||
if self.pipe_abort_read_w is not None:
|
||||
os.close(self.pipe_abort_read_w)
|
||||
self.pipe_abort_read_w = None
|
||||
if self.pipe_abort_read_r is not None:
|
||||
os.close(self.pipe_abort_read_r)
|
||||
self.pipe_abort_read_r = None
|
||||
if self.pipe_abort_write_w is not None:
|
||||
os.close(self.pipe_abort_write_w)
|
||||
self.pipe_abort_write_w = None
|
||||
if self.pipe_abort_write_r is not None:
|
||||
os.close(self.pipe_abort_write_r)
|
||||
self.pipe_abort_write_r = None
|
||||
|
||||
raise
|
||||
|
||||
self.is_open = True
|
||||
|
||||
def _reconfigure_port(self, force_update=False):
|
||||
"""Set communication parameters on opened port."""
|
||||
if self.fd is None:
|
||||
raise SerialException("Can only operate on a valid file descriptor")
|
||||
|
||||
# if exclusive lock is requested, create it before we modify anything else
|
||||
if self._exclusive is not None:
|
||||
if self._exclusive:
|
||||
try:
|
||||
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except IOError as msg:
|
||||
raise SerialException(msg.errno, "Could not exclusively lock port {}: {}".format(self._port, msg))
|
||||
else:
|
||||
fcntl.flock(self.fd, fcntl.LOCK_UN)
|
||||
|
||||
custom_baud = None
|
||||
|
||||
vmin = vtime = 0 # timeout is done via select
|
||||
if self._inter_byte_timeout is not None:
|
||||
vmin = 1
|
||||
vtime = int(self._inter_byte_timeout * 10)
|
||||
try:
|
||||
orig_attr = termios.tcgetattr(self.fd)
|
||||
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
|
||||
except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
|
||||
raise SerialException("Could not configure port: {}".format(msg))
|
||||
# set up raw mode / no echo / binary
|
||||
cflag |= (termios.CLOCAL | termios.CREAD)
|
||||
lflag &= ~(termios.ICANON | termios.ECHO | termios.ECHOE |
|
||||
termios.ECHOK | termios.ECHONL |
|
||||
termios.ISIG | termios.IEXTEN) # |termios.ECHOPRT
|
||||
for flag in ('ECHOCTL', 'ECHOKE'): # netbsd workaround for Erk
|
||||
if hasattr(termios, flag):
|
||||
lflag &= ~getattr(termios, flag)
|
||||
|
||||
oflag &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL)
|
||||
iflag &= ~(termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IGNBRK)
|
||||
if hasattr(termios, 'IUCLC'):
|
||||
iflag &= ~termios.IUCLC
|
||||
if hasattr(termios, 'PARMRK'):
|
||||
iflag &= ~termios.PARMRK
|
||||
|
||||
# setup baud rate
|
||||
try:
|
||||
ispeed = ospeed = getattr(termios, 'B{}'.format(self._baudrate))
|
||||
except AttributeError:
|
||||
try:
|
||||
ispeed = ospeed = self.BAUDRATE_CONSTANTS[self._baudrate]
|
||||
except KeyError:
|
||||
#~ raise ValueError('Invalid baud rate: %r' % self._baudrate)
|
||||
|
||||
# See if BOTHER is defined for this platform; if it is, use
|
||||
# this for a speed not defined in the baudrate constants list.
|
||||
try:
|
||||
ispeed = ospeed = BOTHER
|
||||
except NameError:
|
||||
# may need custom baud rate, it isn't in our list.
|
||||
ispeed = ospeed = getattr(termios, 'B38400')
|
||||
|
||||
try:
|
||||
custom_baud = int(self._baudrate) # store for later
|
||||
except ValueError:
|
||||
raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
|
||||
else:
|
||||
if custom_baud < 0:
|
||||
raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
|
||||
|
||||
# setup char len
|
||||
cflag &= ~termios.CSIZE
|
||||
if self._bytesize == 8:
|
||||
cflag |= termios.CS8
|
||||
elif self._bytesize == 7:
|
||||
cflag |= termios.CS7
|
||||
elif self._bytesize == 6:
|
||||
cflag |= termios.CS6
|
||||
elif self._bytesize == 5:
|
||||
cflag |= termios.CS5
|
||||
else:
|
||||
raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
|
||||
# setup stop bits
|
||||
if self._stopbits == serial.STOPBITS_ONE:
|
||||
cflag &= ~(termios.CSTOPB)
|
||||
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||
cflag |= (termios.CSTOPB) # XXX same as TWO.. there is no POSIX support for 1.5
|
||||
elif self._stopbits == serial.STOPBITS_TWO:
|
||||
cflag |= (termios.CSTOPB)
|
||||
else:
|
||||
raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
|
||||
# setup parity
|
||||
iflag &= ~(termios.INPCK | termios.ISTRIP)
|
||||
if self._parity == serial.PARITY_NONE:
|
||||
cflag &= ~(termios.PARENB | termios.PARODD | CMSPAR)
|
||||
elif self._parity == serial.PARITY_EVEN:
|
||||
cflag &= ~(termios.PARODD | CMSPAR)
|
||||
cflag |= (termios.PARENB)
|
||||
elif self._parity == serial.PARITY_ODD:
|
||||
cflag &= ~CMSPAR
|
||||
cflag |= (termios.PARENB | termios.PARODD)
|
||||
elif self._parity == serial.PARITY_MARK and CMSPAR:
|
||||
cflag |= (termios.PARENB | CMSPAR | termios.PARODD)
|
||||
elif self._parity == serial.PARITY_SPACE and CMSPAR:
|
||||
cflag |= (termios.PARENB | CMSPAR)
|
||||
cflag &= ~(termios.PARODD)
|
||||
else:
|
||||
raise ValueError('Invalid parity: {!r}'.format(self._parity))
|
||||
# setup flow control
|
||||
# xonxoff
|
||||
if hasattr(termios, 'IXANY'):
|
||||
if self._xonxoff:
|
||||
iflag |= (termios.IXON | termios.IXOFF) # |termios.IXANY)
|
||||
else:
|
||||
iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY)
|
||||
else:
|
||||
if self._xonxoff:
|
||||
iflag |= (termios.IXON | termios.IXOFF)
|
||||
else:
|
||||
iflag &= ~(termios.IXON | termios.IXOFF)
|
||||
# rtscts
|
||||
if hasattr(termios, 'CRTSCTS'):
|
||||
if self._rtscts:
|
||||
cflag |= (termios.CRTSCTS)
|
||||
else:
|
||||
cflag &= ~(termios.CRTSCTS)
|
||||
elif hasattr(termios, 'CNEW_RTSCTS'): # try it with alternate constant name
|
||||
if self._rtscts:
|
||||
cflag |= (termios.CNEW_RTSCTS)
|
||||
else:
|
||||
cflag &= ~(termios.CNEW_RTSCTS)
|
||||
# XXX should there be a warning if setting up rtscts (and xonxoff etc) fails??
|
||||
|
||||
# buffer
|
||||
# vmin "minimal number of characters to be read. 0 for non blocking"
|
||||
if vmin < 0 or vmin > 255:
|
||||
raise ValueError('Invalid vmin: {!r}'.format(vmin))
|
||||
cc[termios.VMIN] = vmin
|
||||
# vtime
|
||||
if vtime < 0 or vtime > 255:
|
||||
raise ValueError('Invalid vtime: {!r}'.format(vtime))
|
||||
cc[termios.VTIME] = vtime
|
||||
# activate settings
|
||||
if force_update or [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] != orig_attr:
|
||||
termios.tcsetattr(
|
||||
self.fd,
|
||||
termios.TCSANOW,
|
||||
[iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
|
||||
|
||||
# apply custom baud rate, if any
|
||||
if custom_baud is not None:
|
||||
self._set_special_baudrate(custom_baud)
|
||||
|
||||
if self._rs485_mode is not None:
|
||||
self._set_rs485_mode(self._rs485_mode)
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
if self.fd is not None:
|
||||
os.close(self.fd)
|
||||
self.fd = None
|
||||
os.close(self.pipe_abort_read_w)
|
||||
os.close(self.pipe_abort_read_r)
|
||||
os.close(self.pipe_abort_write_w)
|
||||
os.close(self.pipe_abort_write_r)
|
||||
self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
|
||||
self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of bytes currently in the input buffer."""
|
||||
#~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
|
||||
s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0]
|
||||
|
||||
# select based implementation, proved to work on many systems
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
timeout = Timeout(self._timeout)
|
||||
while len(read) < size:
|
||||
try:
|
||||
ready, _, _ = select.select([self.fd, self.pipe_abort_read_r], [], [], timeout.time_left())
|
||||
if self.pipe_abort_read_r in ready:
|
||||
os.read(self.pipe_abort_read_r, 1000)
|
||||
break
|
||||
# If select was used with a timeout, and the timeout occurs, it
|
||||
# returns with empty lists -> thus abort read operation.
|
||||
# For timeout == 0 (non-blocking operation) also abort when
|
||||
# there is nothing to read.
|
||||
if not ready:
|
||||
break # timeout
|
||||
buf = os.read(self.fd, size - len(read))
|
||||
except OSError as e:
|
||||
# this is for Python 3.x where select.error is a subclass of
|
||||
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||
# https://www.python.org/dev/peps/pep-0475.
|
||||
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
except select.error as e:
|
||||
# this is for Python 2.x
|
||||
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
else:
|
||||
# read should always return some data as select reported it was
|
||||
# ready to read when we get to this point.
|
||||
if not buf:
|
||||
# Disconnected devices, at least on Linux, show the
|
||||
# behavior that they are always ready to read immediately
|
||||
# but reading returns nothing.
|
||||
raise SerialException(
|
||||
'device reports readiness to read but returned no data '
|
||||
'(device disconnected or multiple access on port?)')
|
||||
read.extend(buf)
|
||||
|
||||
if timeout.expired():
|
||||
break
|
||||
return bytes(read)
|
||||
|
||||
def cancel_read(self):
|
||||
if self.is_open:
|
||||
os.write(self.pipe_abort_read_w, b"x")
|
||||
|
||||
def cancel_write(self):
|
||||
if self.is_open:
|
||||
os.write(self.pipe_abort_write_w, b"x")
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given byte string over the serial port."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
d = to_bytes(data)
|
||||
tx_len = length = len(d)
|
||||
timeout = Timeout(self._write_timeout)
|
||||
while tx_len > 0:
|
||||
try:
|
||||
n = os.write(self.fd, d)
|
||||
if timeout.is_non_blocking:
|
||||
# Zero timeout indicates non-blocking - simply return the
|
||||
# number of bytes of data actually written
|
||||
return n
|
||||
elif not timeout.is_infinite:
|
||||
# when timeout is set, use select to wait for being ready
|
||||
# with the time left as timeout
|
||||
if timeout.expired():
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], timeout.time_left())
|
||||
if abort:
|
||||
os.read(self.pipe_abort_write_r, 1000)
|
||||
break
|
||||
if not ready:
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
else:
|
||||
assert timeout.time_left() is None
|
||||
# wait for write operation
|
||||
abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], None)
|
||||
if abort:
|
||||
os.read(self.pipe_abort_write_r, 1)
|
||||
break
|
||||
if not ready:
|
||||
raise SerialException('write failed (select)')
|
||||
d = d[n:]
|
||||
tx_len -= n
|
||||
except SerialException:
|
||||
raise
|
||||
except OSError as e:
|
||||
# this is for Python 3.x where select.error is a subclass of
|
||||
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||
# https://www.python.org/dev/peps/pep-0475.
|
||||
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('write failed: {}'.format(e))
|
||||
except select.error as e:
|
||||
# this is for Python 2.x
|
||||
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('write failed: {}'.format(e))
|
||||
if not timeout.is_non_blocking and timeout.expired():
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
return length - len(d)
|
||||
|
||||
def flush(self):
|
||||
"""\
|
||||
Flush of file like objects. In this case, wait until all data
|
||||
is written.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
termios.tcdrain(self.fd)
|
||||
|
||||
def _reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
termios.tcflush(self.fd, termios.TCIFLUSH)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._reset_input_buffer()
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and discarding all
|
||||
that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
termios.tcflush(self.fd, termios.TCOFLUSH)
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""\
|
||||
Send break condition. Timed, returns to idle state after given
|
||||
duration.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
termios.tcsendbreak(self.fd, int(duration / 0.25))
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if self._rts_state:
|
||||
fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_RTS_str)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_RTS_str)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if self._dtr_state:
|
||||
fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_DTR_str)
|
||||
else:
|
||||
fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_DTR_str)
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_CTS != 0
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_DSR != 0
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_RI != 0
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0] & TIOCM_CD != 0
|
||||
|
||||
# - - platform specific - - - -
|
||||
|
||||
@property
|
||||
def out_waiting(self):
|
||||
"""Return the number of bytes currently in the output buffer."""
|
||||
#~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
|
||||
s = fcntl.ioctl(self.fd, TIOCOUTQ, TIOCM_zero_str)
|
||||
return struct.unpack('I', s)[0]
|
||||
|
||||
def fileno(self):
|
||||
"""\
|
||||
For easier use of the serial port instance with select.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
return self.fd
|
||||
|
||||
def set_input_flow_control(self, enable=True):
|
||||
"""\
|
||||
Manually control flow - when software flow control is enabled.
|
||||
This will send XON (true) or XOFF (false) to the other device.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if enable:
|
||||
termios.tcflow(self.fd, termios.TCION)
|
||||
else:
|
||||
termios.tcflow(self.fd, termios.TCIOFF)
|
||||
|
||||
def set_output_flow_control(self, enable=True):
|
||||
"""\
|
||||
Manually control flow of outgoing data - when hardware or software flow
|
||||
control is enabled.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if enable:
|
||||
termios.tcflow(self.fd, termios.TCOON)
|
||||
else:
|
||||
termios.tcflow(self.fd, termios.TCOOFF)
|
||||
|
||||
def nonblocking(self):
|
||||
"""DEPRECATED - has no use"""
|
||||
import warnings
|
||||
warnings.warn("nonblocking() has no effect, already nonblocking", DeprecationWarning)
|
||||
|
||||
|
||||
class PosixPollSerial(Serial):
|
||||
"""\
|
||||
Poll based read implementation. Not all systems support poll properly.
|
||||
However this one has better handling of errors, such as a device
|
||||
disconnecting while it's in use (e.g. USB-serial unplugged).
|
||||
"""
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
timeout = Timeout(self._timeout)
|
||||
poll = select.poll()
|
||||
poll.register(self.fd, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
|
||||
poll.register(self.pipe_abort_read_r, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
|
||||
if size > 0:
|
||||
while len(read) < size:
|
||||
# print "\tread(): size",size, "have", len(read) #debug
|
||||
# wait until device becomes ready to read (or something fails)
|
||||
for fd, event in poll.poll(None if timeout.is_infinite else (timeout.time_left() * 1000)):
|
||||
if fd == self.pipe_abort_read_r:
|
||||
break
|
||||
if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
|
||||
raise SerialException('device reports error (poll)')
|
||||
# we don't care if it is select.POLLIN or timeout, that's
|
||||
# handled below
|
||||
if fd == self.pipe_abort_read_r:
|
||||
os.read(self.pipe_abort_read_r, 1000)
|
||||
break
|
||||
buf = os.read(self.fd, size - len(read))
|
||||
read.extend(buf)
|
||||
if timeout.expired() \
|
||||
or (self._inter_byte_timeout is not None and self._inter_byte_timeout > 0) and not buf:
|
||||
break # early abort on timeout
|
||||
return bytes(read)
|
||||
|
||||
|
||||
class VTIMESerial(Serial):
|
||||
"""\
|
||||
Implement timeout using vtime of tty device instead of using select.
|
||||
This means that no inter character timeout can be specified and that
|
||||
the error handling is degraded.
|
||||
|
||||
Overall timeout is disabled when inter-character timeout is used.
|
||||
|
||||
Note that this implementation does NOT support cancel_read(), it will
|
||||
just ignore that.
|
||||
"""
|
||||
|
||||
def _reconfigure_port(self, force_update=True):
|
||||
"""Set communication parameters on opened port."""
|
||||
super(VTIMESerial, self)._reconfigure_port()
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # clear O_NONBLOCK
|
||||
|
||||
if self._inter_byte_timeout is not None:
|
||||
vmin = 1
|
||||
vtime = int(self._inter_byte_timeout * 10)
|
||||
elif self._timeout is None:
|
||||
vmin = 1
|
||||
vtime = 0
|
||||
else:
|
||||
vmin = 0
|
||||
vtime = int(self._timeout * 10)
|
||||
try:
|
||||
orig_attr = termios.tcgetattr(self.fd)
|
||||
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
|
||||
except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
|
||||
raise serial.SerialException("Could not configure port: {}".format(msg))
|
||||
|
||||
if vtime < 0 or vtime > 255:
|
||||
raise ValueError('Invalid vtime: {!r}'.format(vtime))
|
||||
cc[termios.VTIME] = vtime
|
||||
cc[termios.VMIN] = vmin
|
||||
|
||||
termios.tcsetattr(
|
||||
self.fd,
|
||||
termios.TCSANOW,
|
||||
[iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
while len(read) < size:
|
||||
buf = os.read(self.fd, size - len(read))
|
||||
if not buf:
|
||||
break
|
||||
read.extend(buf)
|
||||
return bytes(read)
|
||||
|
||||
# hack to make hasattr return false
|
||||
cancel_read = property()
|
||||
@@ -1,697 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Base class and support functions used by various backends.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import io
|
||||
import time
|
||||
|
||||
# ``memoryview`` was introduced in Python 2.7 and ``bytes(some_memoryview)``
|
||||
# isn't returning the contents (very unfortunate). Therefore we need special
|
||||
# cases and test for it. Ensure that there is a ``memoryview`` object for older
|
||||
# Python versions. This is easier than making every test dependent on its
|
||||
# existence.
|
||||
try:
|
||||
memoryview
|
||||
except (NameError, AttributeError):
|
||||
# implementation does not matter as we do not really use it.
|
||||
# it just must not inherit from something else we might care for.
|
||||
class memoryview(object): # pylint: disable=redefined-builtin,invalid-name
|
||||
pass
|
||||
|
||||
try:
|
||||
unicode
|
||||
except (NameError, AttributeError):
|
||||
unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||
|
||||
try:
|
||||
basestring
|
||||
except (NameError, AttributeError):
|
||||
basestring = (str,) # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||
|
||||
|
||||
# "for byte in data" fails for python3 as it returns ints instead of bytes
|
||||
def iterbytes(b):
|
||||
"""Iterate over bytes, returning bytes instead of ints (python3)"""
|
||||
if isinstance(b, memoryview):
|
||||
b = b.tobytes()
|
||||
i = 0
|
||||
while True:
|
||||
a = b[i:i + 1]
|
||||
i += 1
|
||||
if a:
|
||||
yield a
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
# all Python versions prior 3.x convert ``str([17])`` to '[17]' instead of '\x11'
|
||||
# so a simple ``bytes(sequence)`` doesn't work for all versions
|
||||
def to_bytes(seq):
|
||||
"""convert a sequence to a bytes type"""
|
||||
if isinstance(seq, bytes):
|
||||
return seq
|
||||
elif isinstance(seq, bytearray):
|
||||
return bytes(seq)
|
||||
elif isinstance(seq, memoryview):
|
||||
return seq.tobytes()
|
||||
elif isinstance(seq, unicode):
|
||||
raise TypeError('unicode strings are not supported, please encode to bytes: {!r}'.format(seq))
|
||||
else:
|
||||
# handle list of integers and bytes (one or more items) for Python 2 and 3
|
||||
return bytes(bytearray(seq))
|
||||
|
||||
|
||||
# create control bytes
|
||||
XON = to_bytes([17])
|
||||
XOFF = to_bytes([19])
|
||||
|
||||
CR = to_bytes([13])
|
||||
LF = to_bytes([10])
|
||||
|
||||
|
||||
PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = 'N', 'E', 'O', 'M', 'S'
|
||||
STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
|
||||
FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS = (5, 6, 7, 8)
|
||||
|
||||
PARITY_NAMES = {
|
||||
PARITY_NONE: 'None',
|
||||
PARITY_EVEN: 'Even',
|
||||
PARITY_ODD: 'Odd',
|
||||
PARITY_MARK: 'Mark',
|
||||
PARITY_SPACE: 'Space',
|
||||
}
|
||||
|
||||
|
||||
class SerialException(IOError):
|
||||
"""Base class for serial port related exceptions."""
|
||||
|
||||
|
||||
class SerialTimeoutException(SerialException):
|
||||
"""Write timeouts give an exception"""
|
||||
|
||||
|
||||
class PortNotOpenError(SerialException):
|
||||
"""Port is not open"""
|
||||
def __init__(self):
|
||||
super(PortNotOpenError, self).__init__('Attempting to use a port that is not open')
|
||||
|
||||
|
||||
class Timeout(object):
|
||||
"""\
|
||||
Abstraction for timeout operations. Using time.monotonic() if available
|
||||
or time.time() in all other cases.
|
||||
|
||||
The class can also be initialized with 0 or None, in order to support
|
||||
non-blocking and fully blocking I/O operations. The attributes
|
||||
is_non_blocking and is_infinite are set accordingly.
|
||||
"""
|
||||
if hasattr(time, 'monotonic'):
|
||||
# Timeout implementation with time.monotonic(). This function is only
|
||||
# supported by Python 3.3 and above. It returns a time in seconds
|
||||
# (float) just as time.time(), but is not affected by system clock
|
||||
# adjustments.
|
||||
TIME = time.monotonic
|
||||
else:
|
||||
# Timeout implementation with time.time(). This is compatible with all
|
||||
# Python versions but has issues if the clock is adjusted while the
|
||||
# timeout is running.
|
||||
TIME = time.time
|
||||
|
||||
def __init__(self, duration):
|
||||
"""Initialize a timeout with given duration"""
|
||||
self.is_infinite = (duration is None)
|
||||
self.is_non_blocking = (duration == 0)
|
||||
self.duration = duration
|
||||
if duration is not None:
|
||||
self.target_time = self.TIME() + duration
|
||||
else:
|
||||
self.target_time = None
|
||||
|
||||
def expired(self):
|
||||
"""Return a boolean, telling if the timeout has expired"""
|
||||
return self.target_time is not None and self.time_left() <= 0
|
||||
|
||||
def time_left(self):
|
||||
"""Return how many seconds are left until the timeout expires"""
|
||||
if self.is_non_blocking:
|
||||
return 0
|
||||
elif self.is_infinite:
|
||||
return None
|
||||
else:
|
||||
delta = self.target_time - self.TIME()
|
||||
if delta > self.duration:
|
||||
# clock jumped, recalculate
|
||||
self.target_time = self.TIME() + self.duration
|
||||
return self.duration
|
||||
else:
|
||||
return max(0, delta)
|
||||
|
||||
def restart(self, duration):
|
||||
"""\
|
||||
Restart a timeout, only supported if a timeout was already set up
|
||||
before.
|
||||
"""
|
||||
self.duration = duration
|
||||
self.target_time = self.TIME() + duration
|
||||
|
||||
|
||||
class SerialBase(io.RawIOBase):
|
||||
"""\
|
||||
Serial port base class. Provides __init__ function and properties to
|
||||
get/set port settings.
|
||||
"""
|
||||
|
||||
# default values, may be overridden in subclasses that do not support all values
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200, 230400, 460800, 500000,
|
||||
576000, 921600, 1000000, 1152000, 1500000, 2000000, 2500000,
|
||||
3000000, 3500000, 4000000)
|
||||
BYTESIZES = (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
|
||||
PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
|
||||
STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
|
||||
|
||||
def __init__(self,
|
||||
port=None,
|
||||
baudrate=9600,
|
||||
bytesize=EIGHTBITS,
|
||||
parity=PARITY_NONE,
|
||||
stopbits=STOPBITS_ONE,
|
||||
timeout=None,
|
||||
xonxoff=False,
|
||||
rtscts=False,
|
||||
write_timeout=None,
|
||||
dsrdtr=False,
|
||||
inter_byte_timeout=None,
|
||||
exclusive=None,
|
||||
**kwargs):
|
||||
"""\
|
||||
Initialize comm port object. If a "port" is given, then the port will be
|
||||
opened immediately. Otherwise a Serial port object in closed state
|
||||
is returned.
|
||||
"""
|
||||
|
||||
self.is_open = False
|
||||
self.portstr = None
|
||||
self.name = None
|
||||
# correct values are assigned below through properties
|
||||
self._port = None
|
||||
self._baudrate = None
|
||||
self._bytesize = None
|
||||
self._parity = None
|
||||
self._stopbits = None
|
||||
self._timeout = None
|
||||
self._write_timeout = None
|
||||
self._xonxoff = None
|
||||
self._rtscts = None
|
||||
self._dsrdtr = None
|
||||
self._inter_byte_timeout = None
|
||||
self._rs485_mode = None # disabled by default
|
||||
self._rts_state = True
|
||||
self._dtr_state = True
|
||||
self._break_state = False
|
||||
self._exclusive = None
|
||||
|
||||
# assign values using get/set methods using the properties feature
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.parity = parity
|
||||
self.stopbits = stopbits
|
||||
self.timeout = timeout
|
||||
self.write_timeout = write_timeout
|
||||
self.xonxoff = xonxoff
|
||||
self.rtscts = rtscts
|
||||
self.dsrdtr = dsrdtr
|
||||
self.inter_byte_timeout = inter_byte_timeout
|
||||
self.exclusive = exclusive
|
||||
|
||||
# watch for backward compatible kwargs
|
||||
if 'writeTimeout' in kwargs:
|
||||
self.write_timeout = kwargs.pop('writeTimeout')
|
||||
if 'interCharTimeout' in kwargs:
|
||||
self.inter_byte_timeout = kwargs.pop('interCharTimeout')
|
||||
if kwargs:
|
||||
raise ValueError('unexpected keyword arguments: {!r}'.format(kwargs))
|
||||
|
||||
if port is not None:
|
||||
self.open()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# to be implemented by subclasses:
|
||||
# def open(self):
|
||||
# def close(self):
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""\
|
||||
Get the current port setting. The value that was passed on init or using
|
||||
setPort() is passed back.
|
||||
"""
|
||||
return self._port
|
||||
|
||||
@port.setter
|
||||
def port(self, port):
|
||||
"""\
|
||||
Change the port.
|
||||
"""
|
||||
if port is not None and not isinstance(port, basestring):
|
||||
raise ValueError('"port" must be None or a string, not {}'.format(type(port)))
|
||||
was_open = self.is_open
|
||||
if was_open:
|
||||
self.close()
|
||||
self.portstr = port
|
||||
self._port = port
|
||||
self.name = self.portstr
|
||||
if was_open:
|
||||
self.open()
|
||||
|
||||
@property
|
||||
def baudrate(self):
|
||||
"""Get the current baud rate setting."""
|
||||
return self._baudrate
|
||||
|
||||
@baudrate.setter
|
||||
def baudrate(self, baudrate):
|
||||
"""\
|
||||
Change baud rate. It raises a ValueError if the port is open and the
|
||||
baud rate is not possible. If the port is closed, then the value is
|
||||
accepted and the exception is raised when the port is opened.
|
||||
"""
|
||||
try:
|
||||
b = int(baudrate)
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
|
||||
else:
|
||||
if b < 0:
|
||||
raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
|
||||
self._baudrate = b
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def bytesize(self):
|
||||
"""Get the current byte size setting."""
|
||||
return self._bytesize
|
||||
|
||||
@bytesize.setter
|
||||
def bytesize(self, bytesize):
|
||||
"""Change byte size."""
|
||||
if bytesize not in self.BYTESIZES:
|
||||
raise ValueError("Not a valid byte size: {!r}".format(bytesize))
|
||||
self._bytesize = bytesize
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def exclusive(self):
|
||||
"""Get the current exclusive access setting."""
|
||||
return self._exclusive
|
||||
|
||||
@exclusive.setter
|
||||
def exclusive(self, exclusive):
|
||||
"""Change the exclusive access setting."""
|
||||
self._exclusive = exclusive
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def parity(self):
|
||||
"""Get the current parity setting."""
|
||||
return self._parity
|
||||
|
||||
@parity.setter
|
||||
def parity(self, parity):
|
||||
"""Change parity setting."""
|
||||
if parity not in self.PARITIES:
|
||||
raise ValueError("Not a valid parity: {!r}".format(parity))
|
||||
self._parity = parity
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def stopbits(self):
|
||||
"""Get the current stop bits setting."""
|
||||
return self._stopbits
|
||||
|
||||
@stopbits.setter
|
||||
def stopbits(self, stopbits):
|
||||
"""Change stop bits size."""
|
||||
if stopbits not in self.STOPBITS:
|
||||
raise ValueError("Not a valid stop bit size: {!r}".format(stopbits))
|
||||
self._stopbits = stopbits
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
"""Get the current timeout setting."""
|
||||
return self._timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, timeout):
|
||||
"""Change timeout setting."""
|
||||
if timeout is not None:
|
||||
try:
|
||||
timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
if timeout < 0:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
self._timeout = timeout
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def write_timeout(self):
|
||||
"""Get the current timeout setting."""
|
||||
return self._write_timeout
|
||||
|
||||
@write_timeout.setter
|
||||
def write_timeout(self, timeout):
|
||||
"""Change timeout setting."""
|
||||
if timeout is not None:
|
||||
if timeout < 0:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
try:
|
||||
timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||
|
||||
self._write_timeout = timeout
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def inter_byte_timeout(self):
|
||||
"""Get the current inter-character timeout setting."""
|
||||
return self._inter_byte_timeout
|
||||
|
||||
@inter_byte_timeout.setter
|
||||
def inter_byte_timeout(self, ic_timeout):
|
||||
"""Change inter-byte timeout setting."""
|
||||
if ic_timeout is not None:
|
||||
if ic_timeout < 0:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
|
||||
try:
|
||||
ic_timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||
except TypeError:
|
||||
raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
|
||||
|
||||
self._inter_byte_timeout = ic_timeout
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def xonxoff(self):
|
||||
"""Get the current XON/XOFF setting."""
|
||||
return self._xonxoff
|
||||
|
||||
@xonxoff.setter
|
||||
def xonxoff(self, xonxoff):
|
||||
"""Change XON/XOFF setting."""
|
||||
self._xonxoff = xonxoff
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def rtscts(self):
|
||||
"""Get the current RTS/CTS flow control setting."""
|
||||
return self._rtscts
|
||||
|
||||
@rtscts.setter
|
||||
def rtscts(self, rtscts):
|
||||
"""Change RTS/CTS flow control setting."""
|
||||
self._rtscts = rtscts
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def dsrdtr(self):
|
||||
"""Get the current DSR/DTR flow control setting."""
|
||||
return self._dsrdtr
|
||||
|
||||
@dsrdtr.setter
|
||||
def dsrdtr(self, dsrdtr=None):
|
||||
"""Change DsrDtr flow control setting."""
|
||||
if dsrdtr is None:
|
||||
# if not set, keep backwards compatibility and follow rtscts setting
|
||||
self._dsrdtr = self._rtscts
|
||||
else:
|
||||
# if defined independently, follow its value
|
||||
self._dsrdtr = dsrdtr
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
@property
|
||||
def rts(self):
|
||||
return self._rts_state
|
||||
|
||||
@rts.setter
|
||||
def rts(self, value):
|
||||
self._rts_state = value
|
||||
if self.is_open:
|
||||
self._update_rts_state()
|
||||
|
||||
@property
|
||||
def dtr(self):
|
||||
return self._dtr_state
|
||||
|
||||
@dtr.setter
|
||||
def dtr(self, value):
|
||||
self._dtr_state = value
|
||||
if self.is_open:
|
||||
self._update_dtr_state()
|
||||
|
||||
@property
|
||||
def break_condition(self):
|
||||
return self._break_state
|
||||
|
||||
@break_condition.setter
|
||||
def break_condition(self, value):
|
||||
self._break_state = value
|
||||
if self.is_open:
|
||||
self._update_break_state()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# functions useful for RS-485 adapters
|
||||
|
||||
@property
|
||||
def rs485_mode(self):
|
||||
"""\
|
||||
Enable RS485 mode and apply new settings, set to None to disable.
|
||||
See serial.rs485.RS485Settings for more info about the value.
|
||||
"""
|
||||
return self._rs485_mode
|
||||
|
||||
@rs485_mode.setter
|
||||
def rs485_mode(self, rs485_settings):
|
||||
self._rs485_mode = rs485_settings
|
||||
if self.is_open:
|
||||
self._reconfigure_port()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
_SAVED_SETTINGS = ('baudrate', 'bytesize', 'parity', 'stopbits', 'xonxoff',
|
||||
'dsrdtr', 'rtscts', 'timeout', 'write_timeout',
|
||||
'inter_byte_timeout')
|
||||
|
||||
def get_settings(self):
|
||||
"""\
|
||||
Get current port settings as a dictionary. For use with
|
||||
apply_settings().
|
||||
"""
|
||||
return dict([(key, getattr(self, '_' + key)) for key in self._SAVED_SETTINGS])
|
||||
|
||||
def apply_settings(self, d):
|
||||
"""\
|
||||
Apply stored settings from a dictionary returned from
|
||||
get_settings(). It's allowed to delete keys from the dictionary. These
|
||||
values will simply left unchanged.
|
||||
"""
|
||||
for key in self._SAVED_SETTINGS:
|
||||
if key in d and d[key] != getattr(self, '_' + key): # check against internal "_" value
|
||||
setattr(self, key, d[key]) # set non "_" value to use properties write function
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of the current port settings and its state."""
|
||||
return '{name}<id=0x{id:x}, open={p.is_open}>(port={p.portstr!r}, ' \
|
||||
'baudrate={p.baudrate!r}, bytesize={p.bytesize!r}, parity={p.parity!r}, ' \
|
||||
'stopbits={p.stopbits!r}, timeout={p.timeout!r}, xonxoff={p.xonxoff!r}, ' \
|
||||
'rtscts={p.rtscts!r}, dsrdtr={p.dsrdtr!r})'.format(
|
||||
name=self.__class__.__name__, id=id(self), p=self)
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# compatibility with io library
|
||||
# pylint: disable=invalid-name,missing-docstring
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def writable(self):
|
||||
return True
|
||||
|
||||
def seekable(self):
|
||||
return False
|
||||
|
||||
def readinto(self, b):
|
||||
data = self.read(len(b))
|
||||
n = len(data)
|
||||
try:
|
||||
b[:n] = data
|
||||
except TypeError as err:
|
||||
import array
|
||||
if not isinstance(b, array.array):
|
||||
raise err
|
||||
b[:n] = array.array('b', data)
|
||||
return n
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# context manager
|
||||
|
||||
def __enter__(self):
|
||||
if self._port is not None and not self.is_open:
|
||||
self.open()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.close()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""\
|
||||
Send break condition. Timed, returns to idle state after given
|
||||
duration.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self.break_condition = True
|
||||
time.sleep(duration)
|
||||
self.break_condition = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# backwards compatibility / deprecated functions
|
||||
|
||||
def flushInput(self):
|
||||
self.reset_input_buffer()
|
||||
|
||||
def flushOutput(self):
|
||||
self.reset_output_buffer()
|
||||
|
||||
def inWaiting(self):
|
||||
return self.in_waiting
|
||||
|
||||
def sendBreak(self, duration=0.25):
|
||||
self.send_break(duration)
|
||||
|
||||
def setRTS(self, value=1):
|
||||
self.rts = value
|
||||
|
||||
def setDTR(self, value=1):
|
||||
self.dtr = value
|
||||
|
||||
def getCTS(self):
|
||||
return self.cts
|
||||
|
||||
def getDSR(self):
|
||||
return self.dsr
|
||||
|
||||
def getRI(self):
|
||||
return self.ri
|
||||
|
||||
def getCD(self):
|
||||
return self.cd
|
||||
|
||||
def setPort(self, port):
|
||||
self.port = port
|
||||
|
||||
@property
|
||||
def writeTimeout(self):
|
||||
return self.write_timeout
|
||||
|
||||
@writeTimeout.setter
|
||||
def writeTimeout(self, timeout):
|
||||
self.write_timeout = timeout
|
||||
|
||||
@property
|
||||
def interCharTimeout(self):
|
||||
return self.inter_byte_timeout
|
||||
|
||||
@interCharTimeout.setter
|
||||
def interCharTimeout(self, interCharTimeout):
|
||||
self.inter_byte_timeout = interCharTimeout
|
||||
|
||||
def getSettingsDict(self):
|
||||
return self.get_settings()
|
||||
|
||||
def applySettingsDict(self, d):
|
||||
self.apply_settings(d)
|
||||
|
||||
def isOpen(self):
|
||||
return self.is_open
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# additional functionality
|
||||
|
||||
def read_all(self):
|
||||
"""\
|
||||
Read all bytes currently available in the buffer of the OS.
|
||||
"""
|
||||
return self.read(self.in_waiting)
|
||||
|
||||
def read_until(self, expected=LF, size=None):
|
||||
"""\
|
||||
Read until an expected sequence is found ('\n' by default), the size
|
||||
is exceeded or until timeout occurs.
|
||||
"""
|
||||
lenterm = len(expected)
|
||||
line = bytearray()
|
||||
timeout = Timeout(self._timeout)
|
||||
while True:
|
||||
c = self.read(1)
|
||||
if c:
|
||||
line += c
|
||||
if line[-lenterm:] == expected:
|
||||
break
|
||||
if size is not None and len(line) >= size:
|
||||
break
|
||||
else:
|
||||
break
|
||||
if timeout.expired():
|
||||
break
|
||||
return bytes(line)
|
||||
|
||||
def iread_until(self, *args, **kwargs):
|
||||
"""\
|
||||
Read lines, implemented as generator. It will raise StopIteration on
|
||||
timeout (empty read).
|
||||
"""
|
||||
while True:
|
||||
line = self.read_until(*args, **kwargs)
|
||||
if not line:
|
||||
break
|
||||
yield line
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
s = SerialBase()
|
||||
sys.stdout.write('port name: {}\n'.format(s.name))
|
||||
sys.stdout.write('baud rates: {}\n'.format(s.BAUDRATES))
|
||||
sys.stdout.write('byte sizes: {}\n'.format(s.BYTESIZES))
|
||||
sys.stdout.write('parities: {}\n'.format(s.PARITIES))
|
||||
sys.stdout.write('stop bits: {}\n'.format(s.STOPBITS))
|
||||
sys.stdout.write('{}\n'.format(s))
|
||||
@@ -1,477 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# backend for Windows ("win32" incl. 32/64 bit support)
|
||||
#
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# Initial patch to use ctypes by Giovanni Bajo <rasky@develer.com>
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# pylint: disable=invalid-name,too-few-public-methods
|
||||
import ctypes
|
||||
import time
|
||||
from serial import win32
|
||||
|
||||
import serial
|
||||
from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""Serial port implementation for Win32 based on ctypes."""
|
||||
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._port_handle = None
|
||||
self._overlapped_read = None
|
||||
self._overlapped_write = None
|
||||
super(Serial, self).__init__(*args, **kwargs)
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
# the "\\.\COMx" format is required for devices other than COM1-COM8
|
||||
# not all versions of windows seem to support this properly
|
||||
# so that the first few ports are used with the DOS device name
|
||||
port = self.name
|
||||
try:
|
||||
if port.upper().startswith('COM') and int(port[3:]) > 8:
|
||||
port = '\\\\.\\' + port
|
||||
except ValueError:
|
||||
# for like COMnotanumber
|
||||
pass
|
||||
self._port_handle = win32.CreateFile(
|
||||
port,
|
||||
win32.GENERIC_READ | win32.GENERIC_WRITE,
|
||||
0, # exclusive access
|
||||
None, # no security
|
||||
win32.OPEN_EXISTING,
|
||||
win32.FILE_ATTRIBUTE_NORMAL | win32.FILE_FLAG_OVERLAPPED,
|
||||
0)
|
||||
if self._port_handle == win32.INVALID_HANDLE_VALUE:
|
||||
self._port_handle = None # 'cause __del__ is called anyway
|
||||
raise SerialException("could not open port {!r}: {!r}".format(self.portstr, ctypes.WinError()))
|
||||
|
||||
try:
|
||||
self._overlapped_read = win32.OVERLAPPED()
|
||||
self._overlapped_read.hEvent = win32.CreateEvent(None, 1, 0, None)
|
||||
self._overlapped_write = win32.OVERLAPPED()
|
||||
#~ self._overlapped_write.hEvent = win32.CreateEvent(None, 1, 0, None)
|
||||
self._overlapped_write.hEvent = win32.CreateEvent(None, 0, 0, None)
|
||||
|
||||
# Setup a 4k buffer
|
||||
win32.SetupComm(self._port_handle, 4096, 4096)
|
||||
|
||||
# Save original timeout values:
|
||||
self._orgTimeouts = win32.COMMTIMEOUTS()
|
||||
win32.GetCommTimeouts(self._port_handle, ctypes.byref(self._orgTimeouts))
|
||||
|
||||
self._reconfigure_port()
|
||||
|
||||
# Clear buffers:
|
||||
# Remove anything that was there
|
||||
win32.PurgeComm(
|
||||
self._port_handle,
|
||||
win32.PURGE_TXCLEAR | win32.PURGE_TXABORT |
|
||||
win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
|
||||
except:
|
||||
try:
|
||||
self._close()
|
||||
except:
|
||||
# ignore any exception when closing the port
|
||||
# also to keep original exception that happened when setting up
|
||||
pass
|
||||
self._port_handle = None
|
||||
raise
|
||||
else:
|
||||
self.is_open = True
|
||||
|
||||
def _reconfigure_port(self):
|
||||
"""Set communication parameters on opened port."""
|
||||
if not self._port_handle:
|
||||
raise SerialException("Can only operate on a valid port handle")
|
||||
|
||||
# Set Windows timeout values
|
||||
# timeouts is a tuple with the following items:
|
||||
# (ReadIntervalTimeout,ReadTotalTimeoutMultiplier,
|
||||
# ReadTotalTimeoutConstant,WriteTotalTimeoutMultiplier,
|
||||
# WriteTotalTimeoutConstant)
|
||||
timeouts = win32.COMMTIMEOUTS()
|
||||
if self._timeout is None:
|
||||
pass # default of all zeros is OK
|
||||
elif self._timeout == 0:
|
||||
timeouts.ReadIntervalTimeout = win32.MAXDWORD
|
||||
else:
|
||||
timeouts.ReadTotalTimeoutConstant = max(int(self._timeout * 1000), 1)
|
||||
if self._timeout != 0 and self._inter_byte_timeout is not None:
|
||||
timeouts.ReadIntervalTimeout = max(int(self._inter_byte_timeout * 1000), 1)
|
||||
|
||||
if self._write_timeout is None:
|
||||
pass
|
||||
elif self._write_timeout == 0:
|
||||
timeouts.WriteTotalTimeoutConstant = win32.MAXDWORD
|
||||
else:
|
||||
timeouts.WriteTotalTimeoutConstant = max(int(self._write_timeout * 1000), 1)
|
||||
win32.SetCommTimeouts(self._port_handle, ctypes.byref(timeouts))
|
||||
|
||||
win32.SetCommMask(self._port_handle, win32.EV_ERR)
|
||||
|
||||
# Setup the connection info.
|
||||
# Get state and modify it:
|
||||
comDCB = win32.DCB()
|
||||
win32.GetCommState(self._port_handle, ctypes.byref(comDCB))
|
||||
comDCB.BaudRate = self._baudrate
|
||||
|
||||
if self._bytesize == serial.FIVEBITS:
|
||||
comDCB.ByteSize = 5
|
||||
elif self._bytesize == serial.SIXBITS:
|
||||
comDCB.ByteSize = 6
|
||||
elif self._bytesize == serial.SEVENBITS:
|
||||
comDCB.ByteSize = 7
|
||||
elif self._bytesize == serial.EIGHTBITS:
|
||||
comDCB.ByteSize = 8
|
||||
else:
|
||||
raise ValueError("Unsupported number of data bits: {!r}".format(self._bytesize))
|
||||
|
||||
if self._parity == serial.PARITY_NONE:
|
||||
comDCB.Parity = win32.NOPARITY
|
||||
comDCB.fParity = 0 # Disable Parity Check
|
||||
elif self._parity == serial.PARITY_EVEN:
|
||||
comDCB.Parity = win32.EVENPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
elif self._parity == serial.PARITY_ODD:
|
||||
comDCB.Parity = win32.ODDPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
elif self._parity == serial.PARITY_MARK:
|
||||
comDCB.Parity = win32.MARKPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
elif self._parity == serial.PARITY_SPACE:
|
||||
comDCB.Parity = win32.SPACEPARITY
|
||||
comDCB.fParity = 1 # Enable Parity Check
|
||||
else:
|
||||
raise ValueError("Unsupported parity mode: {!r}".format(self._parity))
|
||||
|
||||
if self._stopbits == serial.STOPBITS_ONE:
|
||||
comDCB.StopBits = win32.ONESTOPBIT
|
||||
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||
comDCB.StopBits = win32.ONE5STOPBITS
|
||||
elif self._stopbits == serial.STOPBITS_TWO:
|
||||
comDCB.StopBits = win32.TWOSTOPBITS
|
||||
else:
|
||||
raise ValueError("Unsupported number of stop bits: {!r}".format(self._stopbits))
|
||||
|
||||
comDCB.fBinary = 1 # Enable Binary Transmission
|
||||
# Char. w/ Parity-Err are replaced with 0xff (if fErrorChar is set to TRUE)
|
||||
if self._rs485_mode is None:
|
||||
if self._rtscts:
|
||||
comDCB.fRtsControl = win32.RTS_CONTROL_HANDSHAKE
|
||||
else:
|
||||
comDCB.fRtsControl = win32.RTS_CONTROL_ENABLE if self._rts_state else win32.RTS_CONTROL_DISABLE
|
||||
comDCB.fOutxCtsFlow = self._rtscts
|
||||
else:
|
||||
# checks for unsupported settings
|
||||
# XXX verify if platform really does not have a setting for those
|
||||
if not self._rs485_mode.rts_level_for_tx:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.rts_level_for_tx: {!r} (only True is allowed)'.format(
|
||||
self._rs485_mode.rts_level_for_tx,))
|
||||
if self._rs485_mode.rts_level_for_rx:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.rts_level_for_rx: {!r} (only False is allowed)'.format(
|
||||
self._rs485_mode.rts_level_for_rx,))
|
||||
if self._rs485_mode.delay_before_tx is not None:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.delay_before_tx: {!r} (only None is allowed)'.format(
|
||||
self._rs485_mode.delay_before_tx,))
|
||||
if self._rs485_mode.delay_before_rx is not None:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.delay_before_rx: {!r} (only None is allowed)'.format(
|
||||
self._rs485_mode.delay_before_rx,))
|
||||
if self._rs485_mode.loopback:
|
||||
raise ValueError(
|
||||
'Unsupported value for RS485Settings.loopback: {!r} (only False is allowed)'.format(
|
||||
self._rs485_mode.loopback,))
|
||||
comDCB.fRtsControl = win32.RTS_CONTROL_TOGGLE
|
||||
comDCB.fOutxCtsFlow = 0
|
||||
|
||||
if self._dsrdtr:
|
||||
comDCB.fDtrControl = win32.DTR_CONTROL_HANDSHAKE
|
||||
else:
|
||||
comDCB.fDtrControl = win32.DTR_CONTROL_ENABLE if self._dtr_state else win32.DTR_CONTROL_DISABLE
|
||||
comDCB.fOutxDsrFlow = self._dsrdtr
|
||||
comDCB.fOutX = self._xonxoff
|
||||
comDCB.fInX = self._xonxoff
|
||||
comDCB.fNull = 0
|
||||
comDCB.fErrorChar = 0
|
||||
comDCB.fAbortOnError = 0
|
||||
comDCB.XonChar = serial.XON
|
||||
comDCB.XoffChar = serial.XOFF
|
||||
|
||||
if not win32.SetCommState(self._port_handle, ctypes.byref(comDCB)):
|
||||
raise SerialException(
|
||||
'Cannot configure port, something went wrong. '
|
||||
'Original message: {!r}'.format(ctypes.WinError()))
|
||||
|
||||
#~ def __del__(self):
|
||||
#~ self.close()
|
||||
|
||||
def _close(self):
|
||||
"""internal close port helper"""
|
||||
if self._port_handle is not None:
|
||||
# Restore original timeout values:
|
||||
win32.SetCommTimeouts(self._port_handle, self._orgTimeouts)
|
||||
if self._overlapped_read is not None:
|
||||
self.cancel_read()
|
||||
win32.CloseHandle(self._overlapped_read.hEvent)
|
||||
self._overlapped_read = None
|
||||
if self._overlapped_write is not None:
|
||||
self.cancel_write()
|
||||
win32.CloseHandle(self._overlapped_write.hEvent)
|
||||
self._overlapped_write = None
|
||||
win32.CloseHandle(self._port_handle)
|
||||
self._port_handle = None
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
self._close()
|
||||
self.is_open = False
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of bytes currently in the input buffer."""
|
||||
flags = win32.DWORD()
|
||||
comstat = win32.COMSTAT()
|
||||
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||
return comstat.cbInQue
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if size > 0:
|
||||
win32.ResetEvent(self._overlapped_read.hEvent)
|
||||
flags = win32.DWORD()
|
||||
comstat = win32.COMSTAT()
|
||||
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||
n = min(comstat.cbInQue, size) if self.timeout == 0 else size
|
||||
if n > 0:
|
||||
buf = ctypes.create_string_buffer(n)
|
||||
rc = win32.DWORD()
|
||||
read_ok = win32.ReadFile(
|
||||
self._port_handle,
|
||||
buf,
|
||||
n,
|
||||
ctypes.byref(rc),
|
||||
ctypes.byref(self._overlapped_read))
|
||||
if not read_ok and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||
raise SerialException("ReadFile failed ({!r})".format(ctypes.WinError()))
|
||||
result_ok = win32.GetOverlappedResult(
|
||||
self._port_handle,
|
||||
ctypes.byref(self._overlapped_read),
|
||||
ctypes.byref(rc),
|
||||
True)
|
||||
if not result_ok:
|
||||
if win32.GetLastError() != win32.ERROR_OPERATION_ABORTED:
|
||||
raise SerialException("GetOverlappedResult failed ({!r})".format(ctypes.WinError()))
|
||||
read = buf.raw[:rc.value]
|
||||
else:
|
||||
read = bytes()
|
||||
else:
|
||||
read = bytes()
|
||||
return bytes(read)
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given byte string over the serial port."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
#~ if not isinstance(data, (bytes, bytearray)):
|
||||
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||
# convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
|
||||
data = to_bytes(data)
|
||||
if data:
|
||||
#~ win32event.ResetEvent(self._overlapped_write.hEvent)
|
||||
n = win32.DWORD()
|
||||
success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
|
||||
if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0)
|
||||
if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
|
||||
|
||||
# Wait for the write to complete.
|
||||
#~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE)
|
||||
win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True)
|
||||
if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
|
||||
return n.value # canceled IO is no error
|
||||
if n.value != len(data):
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
return n.value
|
||||
else:
|
||||
errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
|
||||
if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
|
||||
win32.ERROR_OPERATION_ABORTED):
|
||||
return 0
|
||||
elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||
# no info on true length provided by OS function in async mode
|
||||
return len(data)
|
||||
else:
|
||||
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
|
||||
else:
|
||||
return 0
|
||||
|
||||
def flush(self):
|
||||
"""\
|
||||
Flush of file like objects. In this case, wait until all data
|
||||
is written.
|
||||
"""
|
||||
while self.out_waiting:
|
||||
time.sleep(0.05)
|
||||
# XXX could also use WaitCommEvent with mask EV_TXEMPTY, but it would
|
||||
# require overlapped IO and it's also only possible to set a single mask
|
||||
# on the port---
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
win32.PurgeComm(self._port_handle, win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and discarding all
|
||||
that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
win32.PurgeComm(self._port_handle, win32.PURGE_TXCLEAR | win32.PURGE_TXABORT)
|
||||
|
||||
def _update_break_state(self):
|
||||
"""Set break: Controls TXD. When active, to transmitting is possible."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self._break_state:
|
||||
win32.SetCommBreak(self._port_handle)
|
||||
else:
|
||||
win32.ClearCommBreak(self._port_handle)
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if self._rts_state:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETRTS)
|
||||
else:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.CLRRTS)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if self._dtr_state:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETDTR)
|
||||
else:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.CLRDTR)
|
||||
|
||||
def _GetCommModemStatus(self):
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
stat = win32.DWORD()
|
||||
win32.GetCommModemStatus(self._port_handle, ctypes.byref(stat))
|
||||
return stat.value
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
return win32.MS_CTS_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
return win32.MS_DSR_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
return win32.MS_RING_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
return win32.MS_RLSD_ON & self._GetCommModemStatus() != 0
|
||||
|
||||
# - - platform specific - - - -
|
||||
|
||||
def set_buffer_size(self, rx_size=4096, tx_size=None):
|
||||
"""\
|
||||
Recommend a buffer size to the driver (device driver can ignore this
|
||||
value). Must be called after the port is opened.
|
||||
"""
|
||||
if tx_size is None:
|
||||
tx_size = rx_size
|
||||
win32.SetupComm(self._port_handle, rx_size, tx_size)
|
||||
|
||||
def set_output_flow_control(self, enable=True):
|
||||
"""\
|
||||
Manually control flow - when software flow control is enabled.
|
||||
This will do the same as if XON (true) or XOFF (false) are received
|
||||
from the other device and control the transmission accordingly.
|
||||
WARNING: this function is not portable to different platforms!
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if enable:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETXON)
|
||||
else:
|
||||
win32.EscapeCommFunction(self._port_handle, win32.SETXOFF)
|
||||
|
||||
@property
|
||||
def out_waiting(self):
|
||||
"""Return how many bytes the in the outgoing buffer"""
|
||||
flags = win32.DWORD()
|
||||
comstat = win32.COMSTAT()
|
||||
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||
return comstat.cbOutQue
|
||||
|
||||
def _cancel_overlapped_io(self, overlapped):
|
||||
"""Cancel a blocking read operation, may be called from other thread"""
|
||||
# check if read operation is pending
|
||||
rc = win32.DWORD()
|
||||
err = win32.GetOverlappedResult(
|
||||
self._port_handle,
|
||||
ctypes.byref(overlapped),
|
||||
ctypes.byref(rc),
|
||||
False)
|
||||
if not err and win32.GetLastError() in (win32.ERROR_IO_PENDING, win32.ERROR_IO_INCOMPLETE):
|
||||
# cancel, ignoring any errors (e.g. it may just have finished on its own)
|
||||
win32.CancelIoEx(self._port_handle, overlapped)
|
||||
|
||||
def cancel_read(self):
|
||||
"""Cancel a blocking read operation, may be called from other thread"""
|
||||
self._cancel_overlapped_io(self._overlapped_read)
|
||||
|
||||
def cancel_write(self):
|
||||
"""Cancel a blocking write operation, may be called from other thread"""
|
||||
self._cancel_overlapped_io(self._overlapped_write)
|
||||
|
||||
@SerialBase.exclusive.setter
|
||||
def exclusive(self, exclusive):
|
||||
"""Change the exclusive access setting."""
|
||||
if exclusive is not None and not exclusive:
|
||||
raise ValueError('win32 only supports exclusive access (not: {})'.format(exclusive))
|
||||
else:
|
||||
serial.SerialBase.exclusive.__set__(self, exclusive)
|
||||
@@ -1,297 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Working with threading and pySerial
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015-2016 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
"""\
|
||||
Support threading with serial ports.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import serial
|
||||
import threading
|
||||
|
||||
|
||||
class Protocol(object):
|
||||
"""\
|
||||
Protocol as used by the ReaderThread. This base class provides empty
|
||||
implementations of all methods.
|
||||
"""
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Called when reader thread is started"""
|
||||
|
||||
def data_received(self, data):
|
||||
"""Called with snippets received from the serial port"""
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""\
|
||||
Called when the serial port is closed or the reader loop terminated
|
||||
otherwise.
|
||||
"""
|
||||
if isinstance(exc, Exception):
|
||||
raise exc
|
||||
|
||||
|
||||
class Packetizer(Protocol):
|
||||
"""
|
||||
Read binary packets from serial port. Packets are expected to be terminated
|
||||
with a TERMINATOR byte (null byte by default).
|
||||
|
||||
The class also keeps track of the transport.
|
||||
"""
|
||||
|
||||
TERMINATOR = b'\0'
|
||||
|
||||
def __init__(self):
|
||||
self.buffer = bytearray()
|
||||
self.transport = None
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Store transport"""
|
||||
self.transport = transport
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Forget transport"""
|
||||
self.transport = None
|
||||
super(Packetizer, self).connection_lost(exc)
|
||||
|
||||
def data_received(self, data):
|
||||
"""Buffer received data, find TERMINATOR, call handle_packet"""
|
||||
self.buffer.extend(data)
|
||||
while self.TERMINATOR in self.buffer:
|
||||
packet, self.buffer = self.buffer.split(self.TERMINATOR, 1)
|
||||
self.handle_packet(packet)
|
||||
|
||||
def handle_packet(self, packet):
|
||||
"""Process packets - to be overridden by subclassing"""
|
||||
raise NotImplementedError('please implement functionality in handle_packet')
|
||||
|
||||
|
||||
class FramedPacket(Protocol):
|
||||
"""
|
||||
Read binary packets. Packets are expected to have a start and stop marker.
|
||||
|
||||
The class also keeps track of the transport.
|
||||
"""
|
||||
|
||||
START = b'('
|
||||
STOP = b')'
|
||||
|
||||
def __init__(self):
|
||||
self.packet = bytearray()
|
||||
self.in_packet = False
|
||||
self.transport = None
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Store transport"""
|
||||
self.transport = transport
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Forget transport"""
|
||||
self.transport = None
|
||||
self.in_packet = False
|
||||
del self.packet[:]
|
||||
super(FramedPacket, self).connection_lost(exc)
|
||||
|
||||
def data_received(self, data):
|
||||
"""Find data enclosed in START/STOP, call handle_packet"""
|
||||
for byte in serial.iterbytes(data):
|
||||
if byte == self.START:
|
||||
self.in_packet = True
|
||||
elif byte == self.STOP:
|
||||
self.in_packet = False
|
||||
self.handle_packet(bytes(self.packet)) # make read-only copy
|
||||
del self.packet[:]
|
||||
elif self.in_packet:
|
||||
self.packet.extend(byte)
|
||||
else:
|
||||
self.handle_out_of_packet_data(byte)
|
||||
|
||||
def handle_packet(self, packet):
|
||||
"""Process packets - to be overridden by subclassing"""
|
||||
raise NotImplementedError('please implement functionality in handle_packet')
|
||||
|
||||
def handle_out_of_packet_data(self, data):
|
||||
"""Process data that is received outside of packets"""
|
||||
pass
|
||||
|
||||
|
||||
class LineReader(Packetizer):
|
||||
"""
|
||||
Read and write (Unicode) lines from/to serial port.
|
||||
The encoding is applied.
|
||||
"""
|
||||
|
||||
TERMINATOR = b'\r\n'
|
||||
ENCODING = 'utf-8'
|
||||
UNICODE_HANDLING = 'replace'
|
||||
|
||||
def handle_packet(self, packet):
|
||||
self.handle_line(packet.decode(self.ENCODING, self.UNICODE_HANDLING))
|
||||
|
||||
def handle_line(self, line):
|
||||
"""Process one line - to be overridden by subclassing"""
|
||||
raise NotImplementedError('please implement functionality in handle_line')
|
||||
|
||||
def write_line(self, text):
|
||||
"""
|
||||
Write text to the transport. ``text`` is a Unicode string and the encoding
|
||||
is applied before sending ans also the newline is append.
|
||||
"""
|
||||
# + is not the best choice but bytes does not support % or .format in py3 and we want a single write call
|
||||
self.transport.write(text.encode(self.ENCODING, self.UNICODE_HANDLING) + self.TERMINATOR)
|
||||
|
||||
|
||||
class ReaderThread(threading.Thread):
|
||||
"""\
|
||||
Implement a serial port read loop and dispatch to a Protocol instance (like
|
||||
the asyncio.Protocol) but do it with threads.
|
||||
|
||||
Calls to close() will close the serial port but it is also possible to just
|
||||
stop() this thread and continue the serial port instance otherwise.
|
||||
"""
|
||||
|
||||
def __init__(self, serial_instance, protocol_factory):
|
||||
"""\
|
||||
Initialize thread.
|
||||
|
||||
Note that the serial_instance' timeout is set to one second!
|
||||
Other settings are not changed.
|
||||
"""
|
||||
super(ReaderThread, self).__init__()
|
||||
self.daemon = True
|
||||
self.serial = serial_instance
|
||||
self.protocol_factory = protocol_factory
|
||||
self.alive = True
|
||||
self._lock = threading.Lock()
|
||||
self._connection_made = threading.Event()
|
||||
self.protocol = None
|
||||
|
||||
def stop(self):
|
||||
"""Stop the reader thread"""
|
||||
self.alive = False
|
||||
if hasattr(self.serial, 'cancel_read'):
|
||||
self.serial.cancel_read()
|
||||
self.join(2)
|
||||
|
||||
def run(self):
|
||||
"""Reader loop"""
|
||||
if not hasattr(self.serial, 'cancel_read'):
|
||||
self.serial.timeout = 1
|
||||
self.protocol = self.protocol_factory()
|
||||
try:
|
||||
self.protocol.connection_made(self)
|
||||
except Exception as e:
|
||||
self.alive = False
|
||||
self.protocol.connection_lost(e)
|
||||
self._connection_made.set()
|
||||
return
|
||||
error = None
|
||||
self._connection_made.set()
|
||||
while self.alive and self.serial.is_open:
|
||||
try:
|
||||
# read all that is there or wait for one byte (blocking)
|
||||
data = self.serial.read(self.serial.in_waiting or 1)
|
||||
except serial.SerialException as e:
|
||||
# probably some I/O problem such as disconnected USB serial
|
||||
# adapters -> exit
|
||||
error = e
|
||||
break
|
||||
else:
|
||||
if data:
|
||||
# make a separated try-except for called user code
|
||||
try:
|
||||
self.protocol.data_received(data)
|
||||
except Exception as e:
|
||||
error = e
|
||||
break
|
||||
self.alive = False
|
||||
self.protocol.connection_lost(error)
|
||||
self.protocol = None
|
||||
|
||||
def write(self, data):
|
||||
"""Thread safe writing (uses lock)"""
|
||||
with self._lock:
|
||||
return self.serial.write(data)
|
||||
|
||||
def close(self):
|
||||
"""Close the serial port and exit reader thread (uses lock)"""
|
||||
# use the lock to let other threads finish writing
|
||||
with self._lock:
|
||||
# first stop reading, so that closing can be done on idle port
|
||||
self.stop()
|
||||
self.serial.close()
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Wait until connection is set up and return the transport and protocol
|
||||
instances.
|
||||
"""
|
||||
if self.alive:
|
||||
self._connection_made.wait()
|
||||
if not self.alive:
|
||||
raise RuntimeError('connection_lost already called')
|
||||
return (self, self.protocol)
|
||||
else:
|
||||
raise RuntimeError('already stopped')
|
||||
|
||||
# - - context manager, returns protocol
|
||||
|
||||
def __enter__(self):
|
||||
"""\
|
||||
Enter context handler. May raise RuntimeError in case the connection
|
||||
could not be created.
|
||||
"""
|
||||
self.start()
|
||||
self._connection_made.wait()
|
||||
if not self.alive:
|
||||
raise RuntimeError('connection_lost already called')
|
||||
return self.protocol
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Leave context: close port"""
|
||||
self.close()
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
# pylint: disable=wrong-import-position
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
#~ PORT = 'spy:///dev/ttyUSB0'
|
||||
PORT = 'loop://'
|
||||
|
||||
class PrintLines(LineReader):
|
||||
def connection_made(self, transport):
|
||||
super(PrintLines, self).connection_made(transport)
|
||||
sys.stdout.write('port opened\n')
|
||||
self.write_line('hello world')
|
||||
|
||||
def handle_line(self, data):
|
||||
sys.stdout.write('line received: {!r}\n'.format(data))
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if exc:
|
||||
traceback.print_exc(exc)
|
||||
sys.stdout.write('port closed\n')
|
||||
|
||||
ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
|
||||
with ReaderThread(ser, PrintLines) as protocol:
|
||||
protocol.write_line('hello')
|
||||
time.sleep(2)
|
||||
|
||||
# alternative usage
|
||||
ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
|
||||
t = ReaderThread(ser, PrintLines)
|
||||
t.start()
|
||||
transport, protocol = t.connect()
|
||||
protocol.write_line('hello')
|
||||
time.sleep(2)
|
||||
t.close()
|
||||
@@ -1,126 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This is a codec to create and decode hexdumps with spaces between characters. used by miniterm.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015-2016 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
"""\
|
||||
Python 'hex' Codec - 2-digit hex with spaces content transfer encoding.
|
||||
|
||||
Encode and decode may be a bit missleading at first sight...
|
||||
|
||||
The textual representation is a hex dump: e.g. "40 41"
|
||||
The "encoded" data of this is the binary form, e.g. b"@A"
|
||||
|
||||
Therefore decoding is binary to text and thus converting binary data to hex dump.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import codecs
|
||||
import serial
|
||||
|
||||
|
||||
try:
|
||||
unicode
|
||||
except (NameError, AttributeError):
|
||||
unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||
|
||||
|
||||
HEXDIGITS = '0123456789ABCDEF'
|
||||
|
||||
|
||||
# Codec APIs
|
||||
|
||||
def hex_encode(data, errors='strict'):
|
||||
"""'40 41 42' -> b'@ab'"""
|
||||
return (serial.to_bytes([int(h, 16) for h in data.split()]), len(data))
|
||||
|
||||
|
||||
def hex_decode(data, errors='strict'):
|
||||
"""b'@ab' -> '40 41 42'"""
|
||||
return (unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))), len(data))
|
||||
|
||||
|
||||
class Codec(codecs.Codec):
|
||||
def encode(self, data, errors='strict'):
|
||||
"""'40 41 42' -> b'@ab'"""
|
||||
return serial.to_bytes([int(h, 16) for h in data.split()])
|
||||
|
||||
def decode(self, data, errors='strict'):
|
||||
"""b'@ab' -> '40 41 42'"""
|
||||
return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
|
||||
|
||||
|
||||
class IncrementalEncoder(codecs.IncrementalEncoder):
|
||||
"""Incremental hex encoder"""
|
||||
|
||||
def __init__(self, errors='strict'):
|
||||
self.errors = errors
|
||||
self.state = 0
|
||||
|
||||
def reset(self):
|
||||
self.state = 0
|
||||
|
||||
def getstate(self):
|
||||
return self.state
|
||||
|
||||
def setstate(self, state):
|
||||
self.state = state
|
||||
|
||||
def encode(self, data, final=False):
|
||||
"""\
|
||||
Incremental encode, keep track of digits and emit a byte when a pair
|
||||
of hex digits is found. The space is optional unless the error
|
||||
handling is defined to be 'strict'.
|
||||
"""
|
||||
state = self.state
|
||||
encoded = []
|
||||
for c in data.upper():
|
||||
if c in HEXDIGITS:
|
||||
z = HEXDIGITS.index(c)
|
||||
if state:
|
||||
encoded.append(z + (state & 0xf0))
|
||||
state = 0
|
||||
else:
|
||||
state = 0x100 + (z << 4)
|
||||
elif c == ' ': # allow spaces to separate values
|
||||
if state and self.errors == 'strict':
|
||||
raise UnicodeError('odd number of hex digits')
|
||||
state = 0
|
||||
else:
|
||||
if self.errors == 'strict':
|
||||
raise UnicodeError('non-hex digit found: {!r}'.format(c))
|
||||
self.state = state
|
||||
return serial.to_bytes(encoded)
|
||||
|
||||
|
||||
class IncrementalDecoder(codecs.IncrementalDecoder):
|
||||
"""Incremental decoder"""
|
||||
def decode(self, data, final=False):
|
||||
return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
|
||||
|
||||
|
||||
class StreamWriter(Codec, codecs.StreamWriter):
|
||||
"""Combination of hexlify codec and StreamWriter"""
|
||||
|
||||
|
||||
class StreamReader(Codec, codecs.StreamReader):
|
||||
"""Combination of hexlify codec and StreamReader"""
|
||||
|
||||
|
||||
def getregentry():
|
||||
"""encodings module API"""
|
||||
return codecs.CodecInfo(
|
||||
name='hexlify',
|
||||
encode=hex_encode,
|
||||
decode=hex_decode,
|
||||
incrementalencoder=IncrementalEncoder,
|
||||
incrementaldecoder=IncrementalDecoder,
|
||||
streamwriter=StreamWriter,
|
||||
streamreader=StreamReader,
|
||||
#~ _is_text_encoding=True,
|
||||
)
|
||||
@@ -1,110 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Serial port enumeration. Console tool and backend selection.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""\
|
||||
This module will provide a function called comports that returns an
|
||||
iterable (generator or list) that will enumerate available com ports. Note that
|
||||
on some systems non-existent ports may be listed.
|
||||
|
||||
Additionally a grep function is supplied that can be used to search for ports
|
||||
based on their descriptions or hardware ID.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
# chose an implementation, depending on os
|
||||
#~ if sys.platform == 'cli':
|
||||
#~ else:
|
||||
if os.name == 'nt': # sys.platform == 'win32':
|
||||
from serial.tools.list_ports_windows import comports
|
||||
elif os.name == 'posix':
|
||||
from serial.tools.list_ports_posix import comports
|
||||
#~ elif os.name == 'java':
|
||||
else:
|
||||
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
|
||||
def grep(regexp, include_links=False):
|
||||
"""\
|
||||
Search for ports using a regular expression. Port name, description and
|
||||
hardware ID are searched. The function returns an iterable that returns the
|
||||
same tuples as comport() would do.
|
||||
"""
|
||||
r = re.compile(regexp, re.I)
|
||||
for info in comports(include_links):
|
||||
port, desc, hwid = info
|
||||
if r.search(port) or r.search(desc) or r.search(hwid):
|
||||
yield info
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Serial port enumeration')
|
||||
|
||||
parser.add_argument(
|
||||
'regexp',
|
||||
nargs='?',
|
||||
help='only show ports that match this regex')
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='show more messages')
|
||||
|
||||
parser.add_argument(
|
||||
'-q', '--quiet',
|
||||
action='store_true',
|
||||
help='suppress all messages')
|
||||
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
type=int,
|
||||
help='only output the N-th entry')
|
||||
|
||||
parser.add_argument(
|
||||
'-s', '--include-links',
|
||||
action='store_true',
|
||||
help='include entries that are symlinks to real devices')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
hits = 0
|
||||
# get iteraror w/ or w/o filter
|
||||
if args.regexp:
|
||||
if not args.quiet:
|
||||
sys.stderr.write("Filtered list with regexp: {!r}\n".format(args.regexp))
|
||||
iterator = sorted(grep(args.regexp, include_links=args.include_links))
|
||||
else:
|
||||
iterator = sorted(comports(include_links=args.include_links))
|
||||
# list them
|
||||
for n, (port, desc, hwid) in enumerate(iterator, 1):
|
||||
if args.n is None or args.n == n:
|
||||
sys.stdout.write("{:20}\n".format(port))
|
||||
if args.verbose:
|
||||
sys.stdout.write(" desc: {}\n".format(desc))
|
||||
sys.stdout.write(" hwid: {}\n".format(hwid))
|
||||
hits += 1
|
||||
if not args.quiet:
|
||||
if hits:
|
||||
sys.stderr.write("{} ports found\n".format(hits))
|
||||
else:
|
||||
sys.stderr.write("no ports found\n")
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,121 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a helper module for the various platform dependent list_port
|
||||
# implementations.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
|
||||
|
||||
def numsplit(text):
|
||||
"""\
|
||||
Convert string into a list of texts and numbers in order to support a
|
||||
natural sorting.
|
||||
"""
|
||||
result = []
|
||||
for group in re.split(r'(\d+)', text):
|
||||
if group:
|
||||
try:
|
||||
group = int(group)
|
||||
except ValueError:
|
||||
pass
|
||||
result.append(group)
|
||||
return result
|
||||
|
||||
|
||||
class ListPortInfo(object):
|
||||
"""Info collection base class for serial ports"""
|
||||
|
||||
def __init__(self, device, skip_link_detection=False):
|
||||
self.device = device
|
||||
self.name = os.path.basename(device)
|
||||
self.description = 'n/a'
|
||||
self.hwid = 'n/a'
|
||||
# USB specific data
|
||||
self.vid = None
|
||||
self.pid = None
|
||||
self.serial_number = None
|
||||
self.location = None
|
||||
self.manufacturer = None
|
||||
self.product = None
|
||||
self.interface = None
|
||||
# special handling for links
|
||||
if not skip_link_detection and device is not None and os.path.islink(device):
|
||||
self.hwid = 'LINK={}'.format(os.path.realpath(device))
|
||||
|
||||
def usb_description(self):
|
||||
"""return a short string to name the port based on USB info"""
|
||||
if self.interface is not None:
|
||||
return '{} - {}'.format(self.product, self.interface)
|
||||
elif self.product is not None:
|
||||
return self.product
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def usb_info(self):
|
||||
"""return a string with USB related information about device"""
|
||||
return 'USB VID:PID={:04X}:{:04X}{}{}'.format(
|
||||
self.vid or 0,
|
||||
self.pid or 0,
|
||||
' SER={}'.format(self.serial_number) if self.serial_number is not None else '',
|
||||
' LOCATION={}'.format(self.location) if self.location is not None else '')
|
||||
|
||||
def apply_usb_info(self):
|
||||
"""update description and hwid from USB data"""
|
||||
self.description = self.usb_description()
|
||||
self.hwid = self.usb_info()
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, ListPortInfo) and self.device == other.device
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.device)
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, ListPortInfo):
|
||||
raise TypeError('unorderable types: {}() and {}()'.format(
|
||||
type(self).__name__,
|
||||
type(other).__name__))
|
||||
return numsplit(self.device) < numsplit(other.device)
|
||||
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.device, self.description)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Item access: backwards compatible -> (port, desc, hwid)"""
|
||||
if index == 0:
|
||||
return self.device
|
||||
elif index == 1:
|
||||
return self.description
|
||||
elif index == 2:
|
||||
return self.hwid
|
||||
else:
|
||||
raise IndexError('{} > 2'.format(index))
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
def list_links(devices):
|
||||
"""\
|
||||
search all /dev devices and look for symlinks to known ports already
|
||||
listed in devices.
|
||||
"""
|
||||
links = []
|
||||
for device in glob.glob('/dev/*'):
|
||||
if os.path.islink(device) and os.path.realpath(device) in devices:
|
||||
links.append(device)
|
||||
return links
|
||||
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
print(ListPortInfo('dummy'))
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a module that gathers a list of serial ports including details on
|
||||
# GNU/Linux systems.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import glob
|
||||
import os
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
|
||||
class SysFS(list_ports_common.ListPortInfo):
|
||||
"""Wrapper for easy sysfs access and device info"""
|
||||
|
||||
def __init__(self, device):
|
||||
super(SysFS, self).__init__(device)
|
||||
# special handling for links
|
||||
if device is not None and os.path.islink(device):
|
||||
device = os.path.realpath(device)
|
||||
is_link = True
|
||||
else:
|
||||
is_link = False
|
||||
self.usb_device_path = None
|
||||
if os.path.exists('/sys/class/tty/{}/device'.format(self.name)):
|
||||
self.device_path = os.path.realpath('/sys/class/tty/{}/device'.format(self.name))
|
||||
self.subsystem = os.path.basename(os.path.realpath(os.path.join(self.device_path, 'subsystem')))
|
||||
else:
|
||||
self.device_path = None
|
||||
self.subsystem = None
|
||||
# check device type
|
||||
if self.subsystem == 'usb-serial':
|
||||
self.usb_interface_path = os.path.dirname(self.device_path)
|
||||
elif self.subsystem == 'usb':
|
||||
self.usb_interface_path = self.device_path
|
||||
else:
|
||||
self.usb_interface_path = None
|
||||
# fill-in info for USB devices
|
||||
if self.usb_interface_path is not None:
|
||||
self.usb_device_path = os.path.dirname(self.usb_interface_path)
|
||||
|
||||
try:
|
||||
num_if = int(self.read_line(self.usb_device_path, 'bNumInterfaces'))
|
||||
except ValueError:
|
||||
num_if = 1
|
||||
|
||||
self.vid = int(self.read_line(self.usb_device_path, 'idVendor'), 16)
|
||||
self.pid = int(self.read_line(self.usb_device_path, 'idProduct'), 16)
|
||||
self.serial_number = self.read_line(self.usb_device_path, 'serial')
|
||||
if num_if > 1: # multi interface devices like FT4232
|
||||
self.location = os.path.basename(self.usb_interface_path)
|
||||
else:
|
||||
self.location = os.path.basename(self.usb_device_path)
|
||||
|
||||
self.manufacturer = self.read_line(self.usb_device_path, 'manufacturer')
|
||||
self.product = self.read_line(self.usb_device_path, 'product')
|
||||
self.interface = self.read_line(self.usb_interface_path, 'interface')
|
||||
|
||||
if self.subsystem in ('usb', 'usb-serial'):
|
||||
self.apply_usb_info()
|
||||
#~ elif self.subsystem in ('pnp', 'amba'): # PCI based devices, raspi
|
||||
elif self.subsystem == 'pnp': # PCI based devices
|
||||
self.description = self.name
|
||||
self.hwid = self.read_line(self.device_path, 'id')
|
||||
elif self.subsystem == 'amba': # raspi
|
||||
self.description = self.name
|
||||
self.hwid = os.path.basename(self.device_path)
|
||||
|
||||
if is_link:
|
||||
self.hwid += ' LINK={}'.format(device)
|
||||
|
||||
def read_line(self, *args):
|
||||
"""\
|
||||
Helper function to read a single line from a file.
|
||||
One or more parameters are allowed, they are joined with os.path.join.
|
||||
Returns None on errors..
|
||||
"""
|
||||
try:
|
||||
with open(os.path.join(*args)) as f:
|
||||
line = f.readline().strip()
|
||||
return line
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/ttyS*') # built-in serial ports
|
||||
devices.extend(glob.glob('/dev/ttyUSB*')) # usb-serial with own driver
|
||||
devices.extend(glob.glob('/dev/ttyXRUSB*')) # xr-usb-serial port exar (DELL Edge 3001)
|
||||
devices.extend(glob.glob('/dev/ttyACM*')) # usb-serial with CDC-ACM profile
|
||||
devices.extend(glob.glob('/dev/ttyAMA*')) # ARM internal port (raspi)
|
||||
devices.extend(glob.glob('/dev/rfcomm*')) # BT serial devices
|
||||
devices.extend(glob.glob('/dev/ttyAP*')) # Advantech multi-port serial controllers
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [info
|
||||
for info in [SysFS(d) for d in devices]
|
||||
if info.subsystem != "platform"] # hide non-present internal serial ports
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for info in sorted(comports()):
|
||||
print("{0}: {0.subsystem}".format(info))
|
||||
@@ -1,299 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a module that gathers a list of serial ports including details on OSX
|
||||
#
|
||||
# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools
|
||||
# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston
|
||||
# and modifications by cliechti, hoihu, hardkrash
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2013-2020
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
# List all of the callout devices in OS/X by querying IOKit.
|
||||
|
||||
# See the following for a reference of how to do this:
|
||||
# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD
|
||||
|
||||
# More help from darwin_hid.py
|
||||
|
||||
# Also see the 'IORegistryExplorer' for an idea of what we are actually searching
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import ctypes
|
||||
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
iokit = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/IOKit.framework/IOKit')
|
||||
cf = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation')
|
||||
|
||||
# kIOMasterPortDefault is no longer exported in BigSur but no biggie, using NULL works just the same
|
||||
kIOMasterPortDefault = 0 # WAS: ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault")
|
||||
kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault")
|
||||
|
||||
kCFStringEncodingMacRoman = 0
|
||||
kCFStringEncodingUTF8 = 0x08000100
|
||||
|
||||
# defined in `IOKit/usb/USBSpec.h`
|
||||
kUSBVendorString = 'USB Vendor Name'
|
||||
kUSBSerialNumberString = 'USB Serial Number'
|
||||
|
||||
# `io_name_t` defined as `typedef char io_name_t[128];`
|
||||
# in `device/device_types.h`
|
||||
io_name_size = 128
|
||||
|
||||
# defined in `mach/kern_return.h`
|
||||
KERN_SUCCESS = 0
|
||||
# kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h`
|
||||
kern_return_t = ctypes.c_int
|
||||
|
||||
iokit.IOServiceMatching.restype = ctypes.c_void_p
|
||||
|
||||
iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IOServiceGetMatchingServices.restype = kern_return_t
|
||||
|
||||
iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IOServiceGetMatchingServices.restype = kern_return_t
|
||||
|
||||
iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32]
|
||||
iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p
|
||||
|
||||
iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IORegistryEntryGetPath.restype = kern_return_t
|
||||
|
||||
iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IORegistryEntryGetName.restype = kern_return_t
|
||||
|
||||
iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
iokit.IOObjectGetClass.restype = kern_return_t
|
||||
|
||||
iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]
|
||||
|
||||
|
||||
cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32]
|
||||
cf.CFStringCreateWithCString.restype = ctypes.c_void_p
|
||||
|
||||
cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
||||
cf.CFStringGetCStringPtr.restype = ctypes.c_char_p
|
||||
|
||||
cf.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32]
|
||||
cf.CFStringGetCString.restype = ctypes.c_bool
|
||||
|
||||
cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
|
||||
cf.CFNumberGetValue.restype = ctypes.c_void_p
|
||||
|
||||
# void CFRelease ( CFTypeRef cf );
|
||||
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
||||
cf.CFRelease.restype = None
|
||||
|
||||
# CFNumber type defines
|
||||
kCFNumberSInt8Type = 1
|
||||
kCFNumberSInt16Type = 2
|
||||
kCFNumberSInt32Type = 3
|
||||
kCFNumberSInt64Type = 4
|
||||
|
||||
|
||||
def get_string_property(device_type, property):
|
||||
"""
|
||||
Search the given device for the specified string property
|
||||
|
||||
@param device_type Type of Device
|
||||
@param property String to search for
|
||||
@return Python string containing the value, or None if not found.
|
||||
"""
|
||||
key = cf.CFStringCreateWithCString(
|
||||
kCFAllocatorDefault,
|
||||
property.encode("utf-8"),
|
||||
kCFStringEncodingUTF8)
|
||||
|
||||
CFContainer = iokit.IORegistryEntryCreateCFProperty(
|
||||
device_type,
|
||||
key,
|
||||
kCFAllocatorDefault,
|
||||
0)
|
||||
output = None
|
||||
|
||||
if CFContainer:
|
||||
output = cf.CFStringGetCStringPtr(CFContainer, 0)
|
||||
if output is not None:
|
||||
output = output.decode('utf-8')
|
||||
else:
|
||||
buffer = ctypes.create_string_buffer(io_name_size);
|
||||
success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8)
|
||||
if success:
|
||||
output = buffer.value.decode('utf-8')
|
||||
cf.CFRelease(CFContainer)
|
||||
return output
|
||||
|
||||
|
||||
def get_int_property(device_type, property, cf_number_type):
|
||||
"""
|
||||
Search the given device for the specified string property
|
||||
|
||||
@param device_type Device to search
|
||||
@param property String to search for
|
||||
@param cf_number_type CFType number
|
||||
|
||||
@return Python string containing the value, or None if not found.
|
||||
"""
|
||||
key = cf.CFStringCreateWithCString(
|
||||
kCFAllocatorDefault,
|
||||
property.encode("utf-8"),
|
||||
kCFStringEncodingUTF8)
|
||||
|
||||
CFContainer = iokit.IORegistryEntryCreateCFProperty(
|
||||
device_type,
|
||||
key,
|
||||
kCFAllocatorDefault,
|
||||
0)
|
||||
|
||||
if CFContainer:
|
||||
if (cf_number_type == kCFNumberSInt32Type):
|
||||
number = ctypes.c_uint32()
|
||||
elif (cf_number_type == kCFNumberSInt16Type):
|
||||
number = ctypes.c_uint16()
|
||||
cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number))
|
||||
cf.CFRelease(CFContainer)
|
||||
return number.value
|
||||
return None
|
||||
|
||||
def IORegistryEntryGetName(device):
|
||||
devicename = ctypes.create_string_buffer(io_name_size);
|
||||
res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename))
|
||||
if res != KERN_SUCCESS:
|
||||
return None
|
||||
# this works in python2 but may not be valid. Also I don't know if
|
||||
# this encoding is guaranteed. It may be dependent on system locale.
|
||||
return devicename.value.decode('utf-8')
|
||||
|
||||
def IOObjectGetClass(device):
|
||||
classname = ctypes.create_string_buffer(io_name_size)
|
||||
iokit.IOObjectGetClass(device, ctypes.byref(classname))
|
||||
return classname.value
|
||||
|
||||
def GetParentDeviceByType(device, parent_type):
|
||||
""" Find the first parent of a device that implements the parent_type
|
||||
@param IOService Service to inspect
|
||||
@return Pointer to the parent type, or None if it was not found.
|
||||
"""
|
||||
# First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice.
|
||||
parent_type = parent_type.encode('utf-8')
|
||||
while IOObjectGetClass(device) != parent_type:
|
||||
parent = ctypes.c_void_p()
|
||||
response = iokit.IORegistryEntryGetParentEntry(
|
||||
device,
|
||||
"IOService".encode("utf-8"),
|
||||
ctypes.byref(parent))
|
||||
# If we weren't able to find a parent for the device, we're done.
|
||||
if response != KERN_SUCCESS:
|
||||
return None
|
||||
device = parent
|
||||
return device
|
||||
|
||||
|
||||
def GetIOServicesByType(service_type):
|
||||
"""
|
||||
returns iterator over specified service_type
|
||||
"""
|
||||
serial_port_iterator = ctypes.c_void_p()
|
||||
|
||||
iokit.IOServiceGetMatchingServices(
|
||||
kIOMasterPortDefault,
|
||||
iokit.IOServiceMatching(service_type.encode('utf-8')),
|
||||
ctypes.byref(serial_port_iterator))
|
||||
|
||||
services = []
|
||||
while iokit.IOIteratorIsValid(serial_port_iterator):
|
||||
service = iokit.IOIteratorNext(serial_port_iterator)
|
||||
if not service:
|
||||
break
|
||||
services.append(service)
|
||||
iokit.IOObjectRelease(serial_port_iterator)
|
||||
return services
|
||||
|
||||
|
||||
def location_to_string(locationID):
|
||||
"""
|
||||
helper to calculate port and bus number from locationID
|
||||
"""
|
||||
loc = ['{}-'.format(locationID >> 24)]
|
||||
while locationID & 0xf00000:
|
||||
if len(loc) > 1:
|
||||
loc.append('.')
|
||||
loc.append('{}'.format((locationID >> 20) & 0xf))
|
||||
locationID <<= 4
|
||||
return ''.join(loc)
|
||||
|
||||
|
||||
class SuitableSerialInterface(object):
|
||||
pass
|
||||
|
||||
|
||||
def scan_interfaces():
|
||||
"""
|
||||
helper function to scan USB interfaces
|
||||
returns a list of SuitableSerialInterface objects with name and id attributes
|
||||
"""
|
||||
interfaces = []
|
||||
for service in GetIOServicesByType('IOSerialBSDClient'):
|
||||
device = get_string_property(service, "IOCalloutDevice")
|
||||
if device:
|
||||
usb_device = GetParentDeviceByType(service, "IOUSBInterface")
|
||||
if usb_device:
|
||||
name = get_string_property(usb_device, "USB Interface Name") or None
|
||||
locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or ''
|
||||
i = SuitableSerialInterface()
|
||||
i.id = locationID
|
||||
i.name = name
|
||||
interfaces.append(i)
|
||||
return interfaces
|
||||
|
||||
|
||||
def search_for_locationID_in_interfaces(serial_interfaces, locationID):
|
||||
for interface in serial_interfaces:
|
||||
if (interface.id == locationID):
|
||||
return interface.name
|
||||
return None
|
||||
|
||||
|
||||
def comports(include_links=False):
|
||||
# XXX include_links is currently ignored. are links in /dev even supported here?
|
||||
# Scan for all iokit serial ports
|
||||
services = GetIOServicesByType('IOSerialBSDClient')
|
||||
ports = []
|
||||
serial_interfaces = scan_interfaces()
|
||||
for service in services:
|
||||
# First, add the callout device file.
|
||||
device = get_string_property(service, "IOCalloutDevice")
|
||||
if device:
|
||||
info = list_ports_common.ListPortInfo(device)
|
||||
# If the serial port is implemented by IOUSBDevice
|
||||
# NOTE IOUSBDevice was deprecated as of 10.11 and finally on Apple Silicon
|
||||
# devices has been completely removed. Thanks to @oskay for this patch.
|
||||
usb_device = GetParentDeviceByType(service, "IOUSBHostDevice")
|
||||
if not usb_device:
|
||||
usb_device = GetParentDeviceByType(service, "IOUSBDevice")
|
||||
if usb_device:
|
||||
# fetch some useful informations from properties
|
||||
info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type)
|
||||
info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type)
|
||||
info.serial_number = get_string_property(usb_device, kUSBSerialNumberString)
|
||||
# We know this is a usb device, so the
|
||||
# IORegistryEntryName should always be aliased to the
|
||||
# usb product name string descriptor.
|
||||
info.product = IORegistryEntryGetName(usb_device) or 'n/a'
|
||||
info.manufacturer = get_string_property(usb_device, kUSBVendorString)
|
||||
locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type)
|
||||
info.location = location_to_string(locationID)
|
||||
info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID)
|
||||
info.apply_usb_info()
|
||||
ports.append(info)
|
||||
return ports
|
||||
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for port, desc, hwid in sorted(comports()):
|
||||
print("{}: {} [{}]".format(port, desc, hwid))
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This is a module that gathers a list of serial ports on POSIXy systems.
|
||||
# For some specific implementations, see also list_ports_linux, list_ports_osx
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""\
|
||||
The ``comports`` function is expected to return an iterable that yields tuples
|
||||
of 3 strings: port name, human readable description and a hardware ID.
|
||||
|
||||
As currently no method is known to get the second two strings easily, they are
|
||||
currently just identical to the port name.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import glob
|
||||
import sys
|
||||
import os
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
# try to detect the OS so that a device can be selected...
|
||||
plat = sys.platform.lower()
|
||||
|
||||
if plat[:5] == 'linux': # Linux (confirmed) # noqa
|
||||
from serial.tools.list_ports_linux import comports
|
||||
|
||||
elif plat[:6] == 'darwin': # OS X (confirmed)
|
||||
from serial.tools.list_ports_osx import comports
|
||||
|
||||
elif plat == 'cygwin': # cygwin/win32
|
||||
# cygwin accepts /dev/com* in many contexts
|
||||
# (such as 'open' call, explicit 'ls'), but 'glob.glob'
|
||||
# and bare 'ls' do not; so use /dev/ttyS* instead
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/ttyS*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:7] == 'openbsd': # OpenBSD
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/cua*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:3] == 'bsd' or plat[:7] == 'freebsd':
|
||||
def comports(include_links=False):
|
||||
devices = glob.glob('/dev/cua*[!.init][!.lock]')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:6] == 'netbsd': # NetBSD
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/dty*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:4] == 'irix': # IRIX
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/ttyf*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:2] == 'hp': # HP-UX (not tested)
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/tty*p0')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:5] == 'sunos': # Solaris/SunOS
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/tty*c')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
elif plat[:3] == 'aix': # AIX
|
||||
def comports(include_links=False):
|
||||
"""scan for available ports. return a list of device names."""
|
||||
devices = glob.glob('/dev/tty*')
|
||||
if include_links:
|
||||
devices.extend(list_ports_common.list_links(devices))
|
||||
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||
|
||||
else:
|
||||
# platform detection has failed...
|
||||
import serial
|
||||
sys.stderr.write("""\
|
||||
don't know how to enumerate ttys on this system.
|
||||
! I you know how the serial ports are named send this information to
|
||||
! the author of this module:
|
||||
|
||||
sys.platform = {!r}
|
||||
os.name = {!r}
|
||||
pySerial version = {}
|
||||
|
||||
also add the naming scheme of the serial ports and with a bit luck you can get
|
||||
this module running...
|
||||
""".format(sys.platform, os.name, serial.VERSION))
|
||||
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for port, desc, hwid in sorted(comports()):
|
||||
print("{}: {} [{}]".format(port, desc, hwid))
|
||||
@@ -1,427 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Enumerate serial ports on Windows including a human readable description
|
||||
# and hardware information.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2016 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# pylint: disable=invalid-name,too-few-public-methods
|
||||
import re
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL
|
||||
from ctypes.wintypes import HWND
|
||||
from ctypes.wintypes import DWORD
|
||||
from ctypes.wintypes import WORD
|
||||
from ctypes.wintypes import LONG
|
||||
from ctypes.wintypes import ULONG
|
||||
from ctypes.wintypes import HKEY
|
||||
from ctypes.wintypes import BYTE
|
||||
import serial
|
||||
from serial.win32 import ULONG_PTR
|
||||
from serial.tools import list_ports_common
|
||||
|
||||
|
||||
def ValidHandle(value, func, arguments):
|
||||
if value == 0:
|
||||
raise ctypes.WinError()
|
||||
return value
|
||||
|
||||
|
||||
NULL = 0
|
||||
HDEVINFO = ctypes.c_void_p
|
||||
LPCTSTR = ctypes.c_wchar_p
|
||||
PCTSTR = ctypes.c_wchar_p
|
||||
PTSTR = ctypes.c_wchar_p
|
||||
LPDWORD = PDWORD = ctypes.POINTER(DWORD)
|
||||
#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE)
|
||||
LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types
|
||||
|
||||
ACCESS_MASK = DWORD
|
||||
REGSAM = ACCESS_MASK
|
||||
|
||||
|
||||
class GUID(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('Data1', DWORD),
|
||||
('Data2', WORD),
|
||||
('Data3', WORD),
|
||||
('Data4', BYTE * 8),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format(
|
||||
self.Data1,
|
||||
self.Data2,
|
||||
self.Data3,
|
||||
''.join(["{:02x}".format(d) for d in self.Data4[:2]]),
|
||||
''.join(["{:02x}".format(d) for d in self.Data4[2:]]),
|
||||
)
|
||||
|
||||
|
||||
class SP_DEVINFO_DATA(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('cbSize', DWORD),
|
||||
('ClassGuid', GUID),
|
||||
('DevInst', DWORD),
|
||||
('Reserved', ULONG_PTR),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst)
|
||||
|
||||
|
||||
PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA)
|
||||
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p
|
||||
|
||||
setupapi = ctypes.windll.LoadLibrary("setupapi")
|
||||
SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList
|
||||
SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO]
|
||||
SetupDiDestroyDeviceInfoList.restype = BOOL
|
||||
|
||||
SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW
|
||||
SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD]
|
||||
SetupDiClassGuidsFromName.restype = BOOL
|
||||
|
||||
SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo
|
||||
SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA]
|
||||
SetupDiEnumDeviceInfo.restype = BOOL
|
||||
|
||||
SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW
|
||||
SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD]
|
||||
SetupDiGetClassDevs.restype = HDEVINFO
|
||||
SetupDiGetClassDevs.errcheck = ValidHandle
|
||||
|
||||
SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW
|
||||
SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD]
|
||||
SetupDiGetDeviceRegistryProperty.restype = BOOL
|
||||
|
||||
SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW
|
||||
SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD]
|
||||
SetupDiGetDeviceInstanceId.restype = BOOL
|
||||
|
||||
SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey
|
||||
SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM]
|
||||
SetupDiOpenDevRegKey.restype = HKEY
|
||||
|
||||
advapi32 = ctypes.windll.LoadLibrary("Advapi32")
|
||||
RegCloseKey = advapi32.RegCloseKey
|
||||
RegCloseKey.argtypes = [HKEY]
|
||||
RegCloseKey.restype = LONG
|
||||
|
||||
RegQueryValueEx = advapi32.RegQueryValueExW
|
||||
RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD]
|
||||
RegQueryValueEx.restype = LONG
|
||||
|
||||
cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32")
|
||||
CM_Get_Parent = cfgmgr32.CM_Get_Parent
|
||||
CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG]
|
||||
CM_Get_Parent.restype = LONG
|
||||
|
||||
CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW
|
||||
CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG]
|
||||
CM_Get_Device_IDW.restype = LONG
|
||||
|
||||
CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err
|
||||
CM_MapCrToWin32Err.argtypes = [DWORD, DWORD]
|
||||
CM_MapCrToWin32Err.restype = DWORD
|
||||
|
||||
|
||||
DIGCF_PRESENT = 2
|
||||
DIGCF_DEVICEINTERFACE = 16
|
||||
INVALID_HANDLE_VALUE = 0
|
||||
ERROR_INSUFFICIENT_BUFFER = 122
|
||||
ERROR_NOT_FOUND = 1168
|
||||
SPDRP_HARDWAREID = 1
|
||||
SPDRP_FRIENDLYNAME = 12
|
||||
SPDRP_LOCATION_PATHS = 35
|
||||
SPDRP_MFG = 11
|
||||
DICS_FLAG_GLOBAL = 1
|
||||
DIREG_DEV = 0x00000001
|
||||
KEY_READ = 0x20019
|
||||
|
||||
|
||||
MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5
|
||||
|
||||
|
||||
def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0, last_serial_number=None):
|
||||
""" Get the serial number of the parent of a device.
|
||||
|
||||
Args:
|
||||
child_devinst: The device instance handle to get the parent serial number of.
|
||||
child_vid: The vendor ID of the child device.
|
||||
child_pid: The product ID of the child device.
|
||||
depth: The current iteration depth of the USB device tree.
|
||||
"""
|
||||
|
||||
# If the traversal depth is beyond the max, abandon attempting to find the serial number.
|
||||
if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH:
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
# Get the parent device instance.
|
||||
devinst = DWORD()
|
||||
ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0)
|
||||
|
||||
if ret:
|
||||
win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0))
|
||||
|
||||
# If there is no parent available, the child was the root device. We cannot traverse
|
||||
# further.
|
||||
if win_error == ERROR_NOT_FOUND:
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
raise ctypes.WinError(win_error)
|
||||
|
||||
# Get the ID of the parent device and parse it for vendor ID, product ID, and serial number.
|
||||
parentHardwareID = ctypes.create_unicode_buffer(250)
|
||||
|
||||
ret = CM_Get_Device_IDW(
|
||||
devinst,
|
||||
parentHardwareID,
|
||||
ctypes.sizeof(parentHardwareID) - 1,
|
||||
0)
|
||||
|
||||
if ret:
|
||||
raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0)))
|
||||
|
||||
parentHardwareID_str = parentHardwareID.value
|
||||
m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?',
|
||||
parentHardwareID_str,
|
||||
re.I)
|
||||
|
||||
# return early if we have no matches (likely malformed serial, traversed too far)
|
||||
if not m:
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
vid = None
|
||||
pid = None
|
||||
serial_number = None
|
||||
if m.group(1):
|
||||
vid = int(m.group(1), 16)
|
||||
if m.group(3):
|
||||
pid = int(m.group(3), 16)
|
||||
if m.group(7):
|
||||
serial_number = m.group(7)
|
||||
|
||||
# store what we found as a fallback for malformed serial values up the chain
|
||||
found_serial_number = serial_number
|
||||
|
||||
# Check that the USB serial number only contains alpha-numeric characters. It may be a windows
|
||||
# device ID (ephemeral ID).
|
||||
if serial_number and not re.match(r'^\w+$', serial_number):
|
||||
serial_number = None
|
||||
|
||||
if not vid or not pid:
|
||||
# If pid and vid are not available at this device level, continue to the parent.
|
||||
return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
|
||||
|
||||
if pid != child_pid or vid != child_vid:
|
||||
# If the VID or PID has changed, we are no longer looking at the same physical device. The
|
||||
# serial number is unknown.
|
||||
return '' if not last_serial_number else last_serial_number
|
||||
|
||||
# In this case, the vid and pid of the parent device are identical to the child. However, if
|
||||
# there still isn't a serial number available, continue to the next parent.
|
||||
if not serial_number:
|
||||
return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
|
||||
|
||||
# Finally, the VID and PID are identical to the child and a serial number is present, so return
|
||||
# it.
|
||||
return serial_number
|
||||
|
||||
|
||||
def iterate_comports():
|
||||
"""Return a generator that yields descriptions for serial ports"""
|
||||
PortsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
|
||||
ports_guids_size = DWORD()
|
||||
if not SetupDiClassGuidsFromName(
|
||||
"Ports",
|
||||
PortsGUIDs,
|
||||
ctypes.sizeof(PortsGUIDs),
|
||||
ctypes.byref(ports_guids_size)):
|
||||
raise ctypes.WinError()
|
||||
|
||||
ModemsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
|
||||
modems_guids_size = DWORD()
|
||||
if not SetupDiClassGuidsFromName(
|
||||
"Modem",
|
||||
ModemsGUIDs,
|
||||
ctypes.sizeof(ModemsGUIDs),
|
||||
ctypes.byref(modems_guids_size)):
|
||||
raise ctypes.WinError()
|
||||
|
||||
GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value]
|
||||
|
||||
# repeat for all possible GUIDs
|
||||
for index in range(len(GUIDs)):
|
||||
bInterfaceNumber = None
|
||||
g_hdi = SetupDiGetClassDevs(
|
||||
ctypes.byref(GUIDs[index]),
|
||||
None,
|
||||
NULL,
|
||||
DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports
|
||||
|
||||
devinfo = SP_DEVINFO_DATA()
|
||||
devinfo.cbSize = ctypes.sizeof(devinfo)
|
||||
index = 0
|
||||
while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)):
|
||||
index += 1
|
||||
|
||||
# get the real com port name
|
||||
hkey = SetupDiOpenDevRegKey(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
DICS_FLAG_GLOBAL,
|
||||
0,
|
||||
DIREG_DEV, # DIREG_DRV for SW info
|
||||
KEY_READ)
|
||||
port_name_buffer = ctypes.create_unicode_buffer(250)
|
||||
port_name_length = ULONG(ctypes.sizeof(port_name_buffer))
|
||||
RegQueryValueEx(
|
||||
hkey,
|
||||
"PortName",
|
||||
None,
|
||||
None,
|
||||
ctypes.byref(port_name_buffer),
|
||||
ctypes.byref(port_name_length))
|
||||
RegCloseKey(hkey)
|
||||
|
||||
# unfortunately does this method also include parallel ports.
|
||||
# we could check for names starting with COM or just exclude LPT
|
||||
# and hope that other "unknown" names are serial ports...
|
||||
if port_name_buffer.value.startswith('LPT'):
|
||||
continue
|
||||
|
||||
# hardware ID
|
||||
szHardwareID = ctypes.create_unicode_buffer(250)
|
||||
# try to get ID that includes serial number
|
||||
if not SetupDiGetDeviceInstanceId(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
#~ ctypes.byref(szHardwareID),
|
||||
szHardwareID,
|
||||
ctypes.sizeof(szHardwareID) - 1,
|
||||
None):
|
||||
# fall back to more generic hardware ID if that would fail
|
||||
if not SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_HARDWAREID,
|
||||
None,
|
||||
ctypes.byref(szHardwareID),
|
||||
ctypes.sizeof(szHardwareID) - 1,
|
||||
None):
|
||||
# Ignore ERROR_INSUFFICIENT_BUFFER
|
||||
if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
|
||||
raise ctypes.WinError()
|
||||
# stringify
|
||||
szHardwareID_str = szHardwareID.value
|
||||
|
||||
info = list_ports_common.ListPortInfo(port_name_buffer.value, skip_link_detection=True)
|
||||
|
||||
# in case of USB, make a more readable string, similar to that form
|
||||
# that we also generate on other platforms
|
||||
if szHardwareID_str.startswith('USB'):
|
||||
m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I)
|
||||
if m:
|
||||
info.vid = int(m.group(1), 16)
|
||||
if m.group(3):
|
||||
info.pid = int(m.group(3), 16)
|
||||
if m.group(5):
|
||||
bInterfaceNumber = int(m.group(5))
|
||||
|
||||
# Check that the USB serial number only contains alpha-numeric characters. It
|
||||
# may be a windows device ID (ephemeral ID) for composite devices.
|
||||
if m.group(7) and re.match(r'^\w+$', m.group(7)):
|
||||
info.serial_number = m.group(7)
|
||||
else:
|
||||
info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid)
|
||||
|
||||
# calculate a location string
|
||||
loc_path_str = ctypes.create_unicode_buffer(250)
|
||||
if SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_LOCATION_PATHS,
|
||||
None,
|
||||
ctypes.byref(loc_path_str),
|
||||
ctypes.sizeof(loc_path_str) - 1,
|
||||
None):
|
||||
m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value)
|
||||
location = []
|
||||
for g in m:
|
||||
if g.group(1):
|
||||
location.append('{:d}'.format(int(g.group(1)) + 1))
|
||||
else:
|
||||
if len(location) > 1:
|
||||
location.append('.')
|
||||
else:
|
||||
location.append('-')
|
||||
location.append(g.group(2))
|
||||
if bInterfaceNumber is not None:
|
||||
location.append(':{}.{}'.format(
|
||||
'x', # XXX how to determine correct bConfigurationValue?
|
||||
bInterfaceNumber))
|
||||
if location:
|
||||
info.location = ''.join(location)
|
||||
info.hwid = info.usb_info()
|
||||
elif szHardwareID_str.startswith('FTDIBUS'):
|
||||
m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I)
|
||||
if m:
|
||||
info.vid = int(m.group(1), 16)
|
||||
info.pid = int(m.group(2), 16)
|
||||
if m.group(4):
|
||||
info.serial_number = m.group(4)
|
||||
# USB location is hidden by FDTI driver :(
|
||||
info.hwid = info.usb_info()
|
||||
else:
|
||||
info.hwid = szHardwareID_str
|
||||
|
||||
# friendly name
|
||||
szFriendlyName = ctypes.create_unicode_buffer(250)
|
||||
if SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_FRIENDLYNAME,
|
||||
#~ SPDRP_DEVICEDESC,
|
||||
None,
|
||||
ctypes.byref(szFriendlyName),
|
||||
ctypes.sizeof(szFriendlyName) - 1,
|
||||
None):
|
||||
info.description = szFriendlyName.value
|
||||
#~ else:
|
||||
# Ignore ERROR_INSUFFICIENT_BUFFER
|
||||
#~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
|
||||
#~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value))
|
||||
# ignore errors and still include the port in the list, friendly name will be same as port name
|
||||
|
||||
# manufacturer
|
||||
szManufacturer = ctypes.create_unicode_buffer(250)
|
||||
if SetupDiGetDeviceRegistryProperty(
|
||||
g_hdi,
|
||||
ctypes.byref(devinfo),
|
||||
SPDRP_MFG,
|
||||
#~ SPDRP_DEVICEDESC,
|
||||
None,
|
||||
ctypes.byref(szManufacturer),
|
||||
ctypes.sizeof(szManufacturer) - 1,
|
||||
None):
|
||||
info.manufacturer = szManufacturer.value
|
||||
yield info
|
||||
SetupDiDestroyDeviceInfoList(g_hdi)
|
||||
|
||||
|
||||
def comports(include_links=False):
|
||||
"""Return a list of info objects about serial ports"""
|
||||
return list(iterate_comports())
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# test
|
||||
if __name__ == '__main__':
|
||||
for port, desc, hwid in sorted(comports()):
|
||||
print("{}: {} [{}]".format(port, desc, hwid))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This module implements a special URL handler that allows selecting an
|
||||
# alternate implementation provided by some backends.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# URL format: alt://port[?option[=value][&option[=value]]]
|
||||
# options:
|
||||
# - class=X used class named X instead of Serial
|
||||
#
|
||||
# example:
|
||||
# use poll based implementation on Posix (Linux):
|
||||
# python -m serial.tools.miniterm alt:///dev/ttyUSB0?class=PosixPollSerial
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
import serial
|
||||
|
||||
|
||||
def serial_class_for_url(url):
|
||||
"""extract host and port from an URL string"""
|
||||
parts = urlparse.urlsplit(url)
|
||||
if parts.scheme != 'alt':
|
||||
raise serial.SerialException(
|
||||
'expected a string in the form "alt://port[?option[=value][&option[=value]]]": '
|
||||
'not starting with alt:// ({!r})'.format(parts.scheme))
|
||||
class_name = 'Serial'
|
||||
try:
|
||||
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||
if option == 'class':
|
||||
class_name = values[0]
|
||||
else:
|
||||
raise ValueError('unknown option: {!r}'.format(option))
|
||||
except ValueError as e:
|
||||
raise serial.SerialException(
|
||||
'expected a string in the form '
|
||||
'"alt://port[?option[=value][&option[=value]]]": {!r}'.format(e))
|
||||
if not hasattr(serial, class_name):
|
||||
raise ValueError('unknown class: {!r}'.format(class_name))
|
||||
cls = getattr(serial, class_name)
|
||||
if not issubclass(cls, serial.Serial):
|
||||
raise ValueError('class {!r} is not an instance of Serial'.format(class_name))
|
||||
return (''.join([parts.netloc, parts.path]), cls)
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
if __name__ == '__main__':
|
||||
s = serial.serial_for_url('alt:///dev/ttyS0?class=PosixPollSerial')
|
||||
print(s)
|
||||
@@ -1,258 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Backend for Silicon Labs CP2110/4 HID-to-UART devices.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
|
||||
# (C) 2019 Google LLC
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# This backend implements support for HID-to-UART devices manufactured
|
||||
# by Silicon Labs and marketed as CP2110 and CP2114. The
|
||||
# implementation is (mostly) OS-independent and in userland. It relies
|
||||
# on cython-hidapi (https://github.com/trezor/cython-hidapi).
|
||||
|
||||
# The HID-to-UART protocol implemented by CP2110/4 is described in the
|
||||
# AN434 document from Silicon Labs:
|
||||
# https://www.silabs.com/documents/public/application-notes/AN434-CP2110-4-Interface-Specification.pdf
|
||||
|
||||
# TODO items:
|
||||
|
||||
# - rtscts support is configured for hardware flow control, but the
|
||||
# signaling is missing (AN434 suggests this is done through GPIO).
|
||||
# - Cancelling reads and writes is not supported.
|
||||
# - Baudrate validation is not implemented, as it depends on model and configuration.
|
||||
|
||||
import struct
|
||||
import threading
|
||||
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
try:
|
||||
import Queue
|
||||
except ImportError:
|
||||
import queue as Queue
|
||||
|
||||
import hid # hidapi
|
||||
|
||||
import serial
|
||||
from serial.serialutil import SerialBase, SerialException, PortNotOpenError, to_bytes, Timeout
|
||||
|
||||
|
||||
# Report IDs and related constant
|
||||
_REPORT_GETSET_UART_ENABLE = 0x41
|
||||
_DISABLE_UART = 0x00
|
||||
_ENABLE_UART = 0x01
|
||||
|
||||
_REPORT_SET_PURGE_FIFOS = 0x43
|
||||
_PURGE_TX_FIFO = 0x01
|
||||
_PURGE_RX_FIFO = 0x02
|
||||
|
||||
_REPORT_GETSET_UART_CONFIG = 0x50
|
||||
|
||||
_REPORT_SET_TRANSMIT_LINE_BREAK = 0x51
|
||||
_REPORT_SET_STOP_LINE_BREAK = 0x52
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
# This is not quite correct. AN343 specifies that the minimum
|
||||
# baudrate is different between CP2110 and CP2114, and it's halved
|
||||
# when using non-8-bit symbols.
|
||||
BAUDRATES = (300, 375, 600, 1200, 1800, 2400, 4800, 9600, 19200,
|
||||
38400, 57600, 115200, 230400, 460800, 500000, 576000,
|
||||
921600, 1000000)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._hid_handle = None
|
||||
self._read_buffer = None
|
||||
self._thread = None
|
||||
super(Serial, self).__init__(*args, **kwargs)
|
||||
|
||||
def open(self):
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
|
||||
self._read_buffer = Queue.Queue()
|
||||
|
||||
self._hid_handle = hid.device()
|
||||
try:
|
||||
portpath = self.from_url(self.portstr)
|
||||
self._hid_handle.open_path(portpath)
|
||||
except OSError as msg:
|
||||
raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
|
||||
|
||||
try:
|
||||
self._reconfigure_port()
|
||||
except:
|
||||
try:
|
||||
self._hid_handle.close()
|
||||
except:
|
||||
pass
|
||||
self._hid_handle = None
|
||||
raise
|
||||
else:
|
||||
self.is_open = True
|
||||
self._thread = threading.Thread(target=self._hid_read_loop)
|
||||
self._thread.setDaemon(True)
|
||||
self._thread.setName('pySerial CP2110 reader thread for {}'.format(self._port))
|
||||
self._thread.start()
|
||||
|
||||
def from_url(self, url):
|
||||
parts = urlparse.urlsplit(url)
|
||||
if parts.scheme != "cp2110":
|
||||
raise SerialException(
|
||||
'expected a string in the forms '
|
||||
'"cp2110:///dev/hidraw9" or "cp2110://0001:0023:00": '
|
||||
'not starting with cp2110:// {{!r}}'.format(parts.scheme))
|
||||
if parts.netloc: # cp2100://BUS:DEVICE:ENDPOINT, for libusb
|
||||
return parts.netloc.encode('utf-8')
|
||||
return parts.path.encode('utf-8')
|
||||
|
||||
def close(self):
|
||||
self.is_open = False
|
||||
if self._thread:
|
||||
self._thread.join(1) # read timeout is 0.1
|
||||
self._thread = None
|
||||
self._hid_handle.close()
|
||||
self._hid_handle = None
|
||||
|
||||
def _reconfigure_port(self):
|
||||
parity_value = None
|
||||
if self._parity == serial.PARITY_NONE:
|
||||
parity_value = 0x00
|
||||
elif self._parity == serial.PARITY_ODD:
|
||||
parity_value = 0x01
|
||||
elif self._parity == serial.PARITY_EVEN:
|
||||
parity_value = 0x02
|
||||
elif self._parity == serial.PARITY_MARK:
|
||||
parity_value = 0x03
|
||||
elif self._parity == serial.PARITY_SPACE:
|
||||
parity_value = 0x04
|
||||
else:
|
||||
raise ValueError('Invalid parity: {!r}'.format(self._parity))
|
||||
|
||||
if self.rtscts:
|
||||
flow_control_value = 0x01
|
||||
else:
|
||||
flow_control_value = 0x00
|
||||
|
||||
data_bits_value = None
|
||||
if self._bytesize == 5:
|
||||
data_bits_value = 0x00
|
||||
elif self._bytesize == 6:
|
||||
data_bits_value = 0x01
|
||||
elif self._bytesize == 7:
|
||||
data_bits_value = 0x02
|
||||
elif self._bytesize == 8:
|
||||
data_bits_value = 0x03
|
||||
else:
|
||||
raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
|
||||
|
||||
stop_bits_value = None
|
||||
if self._stopbits == serial.STOPBITS_ONE:
|
||||
stop_bits_value = 0x00
|
||||
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||
stop_bits_value = 0x01
|
||||
elif self._stopbits == serial.STOPBITS_TWO:
|
||||
stop_bits_value = 0x01
|
||||
else:
|
||||
raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
|
||||
|
||||
configuration_report = struct.pack(
|
||||
'>BLBBBB',
|
||||
_REPORT_GETSET_UART_CONFIG,
|
||||
self._baudrate,
|
||||
parity_value,
|
||||
flow_control_value,
|
||||
data_bits_value,
|
||||
stop_bits_value)
|
||||
|
||||
self._hid_handle.send_feature_report(configuration_report)
|
||||
|
||||
self._hid_handle.send_feature_report(
|
||||
bytes((_REPORT_GETSET_UART_ENABLE, _ENABLE_UART)))
|
||||
self._update_break_state()
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
return self._read_buffer.qsize()
|
||||
|
||||
def reset_input_buffer(self):
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._hid_handle.send_feature_report(
|
||||
bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_RX_FIFO)))
|
||||
# empty read buffer
|
||||
while self._read_buffer.qsize():
|
||||
self._read_buffer.get(False)
|
||||
|
||||
def reset_output_buffer(self):
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
self._hid_handle.send_feature_report(
|
||||
bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_TX_FIFO)))
|
||||
|
||||
def _update_break_state(self):
|
||||
if not self._hid_handle:
|
||||
raise PortNotOpenError()
|
||||
|
||||
if self._break_state:
|
||||
self._hid_handle.send_feature_report(
|
||||
bytes((_REPORT_SET_TRANSMIT_LINE_BREAK, 0)))
|
||||
else:
|
||||
# Note that while AN434 states "There are no data bytes in
|
||||
# the payload other than the Report ID", either hidapi or
|
||||
# Linux does not seem to send the report otherwise.
|
||||
self._hid_handle.send_feature_report(
|
||||
bytes((_REPORT_SET_STOP_LINE_BREAK, 0)))
|
||||
|
||||
def read(self, size=1):
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
|
||||
data = bytearray()
|
||||
try:
|
||||
timeout = Timeout(self._timeout)
|
||||
while len(data) < size:
|
||||
if self._thread is None:
|
||||
raise SerialException('connection failed (reader thread died)')
|
||||
buf = self._read_buffer.get(True, timeout.time_left())
|
||||
if buf is None:
|
||||
return bytes(data)
|
||||
data += buf
|
||||
if timeout.expired():
|
||||
break
|
||||
except Queue.Empty: # -> timeout
|
||||
pass
|
||||
return bytes(data)
|
||||
|
||||
def write(self, data):
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
data = to_bytes(data)
|
||||
tx_len = len(data)
|
||||
while tx_len > 0:
|
||||
to_be_sent = min(tx_len, 0x3F)
|
||||
report = to_bytes([to_be_sent]) + data[:to_be_sent]
|
||||
self._hid_handle.write(report)
|
||||
|
||||
data = data[to_be_sent:]
|
||||
tx_len = len(data)
|
||||
|
||||
def _hid_read_loop(self):
|
||||
try:
|
||||
while self.is_open:
|
||||
data = self._hid_handle.read(64, timeout_ms=100)
|
||||
if not data:
|
||||
continue
|
||||
data_len = data.pop(0)
|
||||
assert data_len == len(data)
|
||||
self._read_buffer.put(bytearray(data))
|
||||
finally:
|
||||
self._thread = None
|
||||
@@ -1,91 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This module implements a special URL handler that uses the port listing to
|
||||
# find ports by searching the string descriptions.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# URL format: hwgrep://<regexp>&<option>
|
||||
#
|
||||
# where <regexp> is a Python regexp according to the re module
|
||||
#
|
||||
# violating the normal definition for URLs, the charachter `&` is used to
|
||||
# separate parameters from the arguments (instead of `?`, but the question mark
|
||||
# is heavily used in regexp'es)
|
||||
#
|
||||
# options:
|
||||
# n=<N> pick the N'th entry instead of the first one (numbering starts at 1)
|
||||
# skip_busy tries to open port to check if it is busy, fails on posix as ports are not locked!
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
|
||||
try:
|
||||
basestring
|
||||
except NameError:
|
||||
basestring = str # python 3 pylint: disable=redefined-builtin
|
||||
|
||||
|
||||
class Serial(serial.Serial):
|
||||
"""Just inherit the native Serial port implementation and patch the port property."""
|
||||
# pylint: disable=no-member
|
||||
|
||||
@serial.Serial.port.setter
|
||||
def port(self, value):
|
||||
"""translate port name before storing it"""
|
||||
if isinstance(value, basestring) and value.startswith('hwgrep://'):
|
||||
serial.Serial.port.__set__(self, self.from_url(value))
|
||||
else:
|
||||
serial.Serial.port.__set__(self, value)
|
||||
|
||||
def from_url(self, url):
|
||||
"""extract host and port from an URL string"""
|
||||
if url.lower().startswith("hwgrep://"):
|
||||
url = url[9:]
|
||||
n = 0
|
||||
test_open = False
|
||||
args = url.split('&')
|
||||
regexp = args.pop(0)
|
||||
for arg in args:
|
||||
if '=' in arg:
|
||||
option, value = arg.split('=', 1)
|
||||
else:
|
||||
option = arg
|
||||
value = None
|
||||
if option == 'n':
|
||||
# pick n'th element
|
||||
n = int(value) - 1
|
||||
if n < 1:
|
||||
raise ValueError('option "n" expects a positive integer larger than 1: {!r}'.format(value))
|
||||
elif option == 'skip_busy':
|
||||
# open to test if port is available. not the nicest way..
|
||||
test_open = True
|
||||
else:
|
||||
raise ValueError('unknown option: {!r}'.format(option))
|
||||
# use a for loop to get the 1st element from the generator
|
||||
for port, desc, hwid in sorted(serial.tools.list_ports.grep(regexp)):
|
||||
if test_open:
|
||||
try:
|
||||
s = serial.Serial(port)
|
||||
except serial.SerialException:
|
||||
# it has some error, skip this one
|
||||
continue
|
||||
else:
|
||||
s.close()
|
||||
if n:
|
||||
n -= 1
|
||||
continue
|
||||
return port
|
||||
else:
|
||||
raise serial.SerialException('no ports found matching regexp {!r}'.format(url))
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
if __name__ == '__main__':
|
||||
s = Serial(None)
|
||||
s.port = 'hwgrep://ttyS0'
|
||||
print(s)
|
||||
@@ -1,308 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This module implements a loop back connection receiving itself what it sent.
|
||||
#
|
||||
# The purpose of this module is.. well... You can run the unit tests with it.
|
||||
# and it was so easy to implement ;-)
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# URL format: loop://[option[/option...]]
|
||||
# options:
|
||||
# - "debug" print diagnostic messages
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import numbers
|
||||
import time
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
from serial.serialutil import SerialBase, SerialException, to_bytes, iterbytes, SerialTimeoutException, PortNotOpenError
|
||||
|
||||
# map log level names to constants. used in from_url()
|
||||
LOGGER_LEVELS = {
|
||||
'debug': logging.DEBUG,
|
||||
'info': logging.INFO,
|
||||
'warning': logging.WARNING,
|
||||
'error': logging.ERROR,
|
||||
}
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""Serial port implementation that simulates a loop back connection in plain software."""
|
||||
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.buffer_size = 4096
|
||||
self.queue = None
|
||||
self.logger = None
|
||||
self._cancel_write = False
|
||||
super(Serial, self).__init__(*args, **kwargs)
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
self.logger = None
|
||||
self.queue = queue.Queue(self.buffer_size)
|
||||
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
# not that there is anything to open, but the function applies the
|
||||
# options found in the URL
|
||||
self.from_url(self.port)
|
||||
|
||||
# not that there anything to configure...
|
||||
self._reconfigure_port()
|
||||
# all things set up get, now a clean start
|
||||
self.is_open = True
|
||||
if not self._dsrdtr:
|
||||
self._update_dtr_state()
|
||||
if not self._rtscts:
|
||||
self._update_rts_state()
|
||||
self.reset_input_buffer()
|
||||
self.reset_output_buffer()
|
||||
|
||||
def close(self):
|
||||
if self.is_open:
|
||||
self.is_open = False
|
||||
try:
|
||||
self.queue.put_nowait(None)
|
||||
except queue.Full:
|
||||
pass
|
||||
super(Serial, self).close()
|
||||
|
||||
def _reconfigure_port(self):
|
||||
"""\
|
||||
Set communication parameters on opened port. For the loop://
|
||||
protocol all settings are ignored!
|
||||
"""
|
||||
# not that's it of any real use, but it helps in the unit tests
|
||||
if not isinstance(self._baudrate, numbers.Integral) or not 0 < self._baudrate < 2 ** 32:
|
||||
raise ValueError("invalid baudrate: {!r}".format(self._baudrate))
|
||||
if self.logger:
|
||||
self.logger.info('_reconfigure_port()')
|
||||
|
||||
def from_url(self, url):
|
||||
"""extract host and port from an URL string"""
|
||||
parts = urlparse.urlsplit(url)
|
||||
if parts.scheme != "loop":
|
||||
raise SerialException(
|
||||
'expected a string in the form '
|
||||
'"loop://[?logging={debug|info|warning|error}]": not starting '
|
||||
'with loop:// ({!r})'.format(parts.scheme))
|
||||
try:
|
||||
# process options now, directly altering self
|
||||
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||
if option == 'logging':
|
||||
logging.basicConfig() # XXX is that good to call it here?
|
||||
self.logger = logging.getLogger('pySerial.loop')
|
||||
self.logger.setLevel(LOGGER_LEVELS[values[0]])
|
||||
self.logger.debug('enabled logging')
|
||||
else:
|
||||
raise ValueError('unknown option: {!r}'.format(option))
|
||||
except ValueError as e:
|
||||
raise SerialException(
|
||||
'expected a string in the form '
|
||||
'"loop://[?logging={debug|info|warning|error}]": {}'.format(e))
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of bytes currently in the input buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
# attention the logged value can differ from return value in
|
||||
# threaded environments...
|
||||
self.logger.debug('in_waiting -> {:d}'.format(self.queue.qsize()))
|
||||
return self.queue.qsize()
|
||||
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self._timeout is not None and self._timeout != 0:
|
||||
timeout = time.time() + self._timeout
|
||||
else:
|
||||
timeout = None
|
||||
data = bytearray()
|
||||
while size > 0 and self.is_open:
|
||||
try:
|
||||
b = self.queue.get(timeout=self._timeout) # XXX inter char timeout
|
||||
except queue.Empty:
|
||||
if self._timeout == 0:
|
||||
break
|
||||
else:
|
||||
if b is not None:
|
||||
data += b
|
||||
size -= 1
|
||||
else:
|
||||
break
|
||||
# check for timeout now, after data has been read.
|
||||
# useful for timeout = 0 (non blocking) read
|
||||
if timeout and time.time() > timeout:
|
||||
if self.logger:
|
||||
self.logger.info('read timeout')
|
||||
break
|
||||
return bytes(data)
|
||||
|
||||
def cancel_read(self):
|
||||
self.queue.put_nowait(None)
|
||||
|
||||
def cancel_write(self):
|
||||
self._cancel_write = True
|
||||
|
||||
def write(self, data):
|
||||
"""\
|
||||
Output the given byte string over the serial port. Can block if the
|
||||
connection is blocked. May raise SerialException if the connection is
|
||||
closed.
|
||||
"""
|
||||
self._cancel_write = False
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
data = to_bytes(data)
|
||||
# calculate aprox time that would be used to send the data
|
||||
time_used_to_send = 10.0 * len(data) / self._baudrate
|
||||
# when a write timeout is configured check if we would be successful
|
||||
# (not sending anything, not even the part that would have time)
|
||||
if self._write_timeout is not None and time_used_to_send > self._write_timeout:
|
||||
# must wait so that unit test succeeds
|
||||
time_left = self._write_timeout
|
||||
while time_left > 0 and not self._cancel_write:
|
||||
time.sleep(min(time_left, 0.5))
|
||||
time_left -= 0.5
|
||||
if self._cancel_write:
|
||||
return 0 # XXX
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
for byte in iterbytes(data):
|
||||
self.queue.put(byte, timeout=self._write_timeout)
|
||||
return len(data)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('reset_input_buffer()')
|
||||
try:
|
||||
while self.queue.qsize():
|
||||
self.queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('reset_output_buffer()')
|
||||
try:
|
||||
while self.queue.qsize():
|
||||
self.queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
@property
|
||||
def out_waiting(self):
|
||||
"""Return how many bytes the in the outgoing buffer"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
# attention the logged value can differ from return value in
|
||||
# threaded environments...
|
||||
self.logger.debug('out_waiting -> {:d}'.format(self.queue.qsize()))
|
||||
return self.queue.qsize()
|
||||
|
||||
def _update_break_state(self):
|
||||
"""\
|
||||
Set break: Controls TXD. When active, to transmitting is
|
||||
possible.
|
||||
"""
|
||||
if self.logger:
|
||||
self.logger.info('_update_break_state({!r})'.format(self._break_state))
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if self.logger:
|
||||
self.logger.info('_update_rts_state({!r}) -> state of CTS'.format(self._rts_state))
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if self.logger:
|
||||
self.logger.info('_update_dtr_state({!r}) -> state of DSR'.format(self._dtr_state))
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('CTS -> state of RTS ({!r})'.format(self._rts_state))
|
||||
return self._rts_state
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if self.logger:
|
||||
self.logger.info('DSR -> state of DTR ({!r})'.format(self._dtr_state))
|
||||
return self._dtr_state
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('returning dummy for RI')
|
||||
return False
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('returning dummy for CD')
|
||||
return True
|
||||
|
||||
# - - - platform specific - - -
|
||||
# None so far
|
||||
|
||||
|
||||
# simple client test
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
s = Serial('loop://')
|
||||
sys.stdout.write('{}\n'.format(s))
|
||||
|
||||
sys.stdout.write("write...\n")
|
||||
s.write("hello\n")
|
||||
s.flush()
|
||||
sys.stdout.write("read: {!r}\n".format(s.read(5)))
|
||||
|
||||
s.close()
|
||||
@@ -1,12 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This is a thin wrapper to load the rfc2217 implementation.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2011 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from serial.rfc2217 import Serial # noqa
|
||||
@@ -1,359 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This module implements a simple socket based client.
|
||||
# It does not support changing any port parameters and will silently ignore any
|
||||
# requests to do so.
|
||||
#
|
||||
# The purpose of this module is that applications using pySerial can connect to
|
||||
# TCP/IP to serial port converters that do not support RFC 2217.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# URL format: socket://<host>:<port>[/option[/option...]]
|
||||
# options:
|
||||
# - "debug" print diagnostic messages
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
from serial.serialutil import SerialBase, SerialException, to_bytes, \
|
||||
PortNotOpenError, SerialTimeoutException, Timeout
|
||||
|
||||
# map log level names to constants. used in from_url()
|
||||
LOGGER_LEVELS = {
|
||||
'debug': logging.DEBUG,
|
||||
'info': logging.INFO,
|
||||
'warning': logging.WARNING,
|
||||
'error': logging.ERROR,
|
||||
}
|
||||
|
||||
POLL_TIMEOUT = 5
|
||||
|
||||
|
||||
class Serial(SerialBase):
|
||||
"""Serial port implementation for plain sockets."""
|
||||
|
||||
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||
9600, 19200, 38400, 57600, 115200)
|
||||
|
||||
def open(self):
|
||||
"""\
|
||||
Open port with current settings. This may throw a SerialException
|
||||
if the port cannot be opened.
|
||||
"""
|
||||
self.logger = None
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before it can be used.")
|
||||
if self.is_open:
|
||||
raise SerialException("Port is already open.")
|
||||
try:
|
||||
# timeout is used for write timeout support :/ and to get an initial connection timeout
|
||||
self._socket = socket.create_connection(self.from_url(self.portstr), timeout=POLL_TIMEOUT)
|
||||
except Exception as msg:
|
||||
self._socket = None
|
||||
raise SerialException("Could not open port {}: {}".format(self.portstr, msg))
|
||||
# after connecting, switch to non-blocking, we're using select
|
||||
self._socket.setblocking(False)
|
||||
|
||||
# not that there is anything to configure...
|
||||
self._reconfigure_port()
|
||||
# all things set up get, now a clean start
|
||||
self.is_open = True
|
||||
if not self._dsrdtr:
|
||||
self._update_dtr_state()
|
||||
if not self._rtscts:
|
||||
self._update_rts_state()
|
||||
self.reset_input_buffer()
|
||||
self.reset_output_buffer()
|
||||
|
||||
def _reconfigure_port(self):
|
||||
"""\
|
||||
Set communication parameters on opened port. For the socket://
|
||||
protocol all settings are ignored!
|
||||
"""
|
||||
if self._socket is None:
|
||||
raise SerialException("Can only operate on open ports")
|
||||
if self.logger:
|
||||
self.logger.info('ignored port configuration change')
|
||||
|
||||
def close(self):
|
||||
"""Close port"""
|
||||
if self.is_open:
|
||||
if self._socket:
|
||||
try:
|
||||
self._socket.shutdown(socket.SHUT_RDWR)
|
||||
self._socket.close()
|
||||
except:
|
||||
# ignore errors.
|
||||
pass
|
||||
self._socket = None
|
||||
self.is_open = False
|
||||
# in case of quick reconnects, give the server some time
|
||||
time.sleep(0.3)
|
||||
|
||||
def from_url(self, url):
|
||||
"""extract host and port from an URL string"""
|
||||
parts = urlparse.urlsplit(url)
|
||||
if parts.scheme != "socket":
|
||||
raise SerialException(
|
||||
'expected a string in the form '
|
||||
'"socket://<host>:<port>[?logging={debug|info|warning|error}]": '
|
||||
'not starting with socket:// ({!r})'.format(parts.scheme))
|
||||
try:
|
||||
# process options now, directly altering self
|
||||
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||
if option == 'logging':
|
||||
logging.basicConfig() # XXX is that good to call it here?
|
||||
self.logger = logging.getLogger('pySerial.socket')
|
||||
self.logger.setLevel(LOGGER_LEVELS[values[0]])
|
||||
self.logger.debug('enabled logging')
|
||||
else:
|
||||
raise ValueError('unknown option: {!r}'.format(option))
|
||||
if not 0 <= parts.port < 65536:
|
||||
raise ValueError("port not in range 0...65535")
|
||||
except ValueError as e:
|
||||
raise SerialException(
|
||||
'expected a string in the form '
|
||||
'"socket://<host>:<port>[?logging={debug|info|warning|error}]": {}'.format(e))
|
||||
|
||||
return (parts.hostname, parts.port)
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of bytes currently in the input buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
# Poll the socket to see if it is ready for reading.
|
||||
# If ready, at least one byte will be to read.
|
||||
lr, lw, lx = select.select([self._socket], [], [], 0)
|
||||
return len(lr)
|
||||
|
||||
# select based implementation, similar to posix, but only using socket API
|
||||
# to be portable, additionally handle socket timeout which is used to
|
||||
# emulate write timeouts
|
||||
def read(self, size=1):
|
||||
"""\
|
||||
Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
read = bytearray()
|
||||
timeout = Timeout(self._timeout)
|
||||
while len(read) < size:
|
||||
try:
|
||||
ready, _, _ = select.select([self._socket], [], [], timeout.time_left())
|
||||
# If select was used with a timeout, and the timeout occurs, it
|
||||
# returns with empty lists -> thus abort read operation.
|
||||
# For timeout == 0 (non-blocking operation) also abort when
|
||||
# there is nothing to read.
|
||||
if not ready:
|
||||
break # timeout
|
||||
buf = self._socket.recv(size - len(read))
|
||||
# read should always return some data as select reported it was
|
||||
# ready to read when we get to this point, unless it is EOF
|
||||
if not buf:
|
||||
raise SerialException('socket disconnected')
|
||||
read.extend(buf)
|
||||
except OSError as e:
|
||||
# this is for Python 3.x where select.error is a subclass of
|
||||
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||
# https://www.python.org/dev/peps/pep-0475.
|
||||
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
except (select.error, socket.error) as e:
|
||||
# this is for Python 2.x
|
||||
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
if timeout.expired():
|
||||
break
|
||||
return bytes(read)
|
||||
|
||||
def write(self, data):
|
||||
"""\
|
||||
Output the given byte string over the serial port. Can block if the
|
||||
connection is blocked. May raise SerialException if the connection is
|
||||
closed.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
|
||||
d = to_bytes(data)
|
||||
tx_len = length = len(d)
|
||||
timeout = Timeout(self._write_timeout)
|
||||
while tx_len > 0:
|
||||
try:
|
||||
n = self._socket.send(d)
|
||||
if timeout.is_non_blocking:
|
||||
# Zero timeout indicates non-blocking - simply return the
|
||||
# number of bytes of data actually written
|
||||
return n
|
||||
elif not timeout.is_infinite:
|
||||
# when timeout is set, use select to wait for being ready
|
||||
# with the time left as timeout
|
||||
if timeout.expired():
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
_, ready, _ = select.select([], [self._socket], [], timeout.time_left())
|
||||
if not ready:
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
else:
|
||||
assert timeout.time_left() is None
|
||||
# wait for write operation
|
||||
_, ready, _ = select.select([], [self._socket], [], None)
|
||||
if not ready:
|
||||
raise SerialException('write failed (select)')
|
||||
d = d[n:]
|
||||
tx_len -= n
|
||||
except SerialException:
|
||||
raise
|
||||
except OSError as e:
|
||||
# this is for Python 3.x where select.error is a subclass of
|
||||
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||
# https://www.python.org/dev/peps/pep-0475.
|
||||
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('write failed: {}'.format(e))
|
||||
except select.error as e:
|
||||
# this is for Python 2.x
|
||||
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('write failed: {}'.format(e))
|
||||
if not timeout.is_non_blocking and timeout.expired():
|
||||
raise SerialTimeoutException('Write timeout')
|
||||
return length - len(d)
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
|
||||
# just use recv to remove input, while there is some
|
||||
ready = True
|
||||
while ready:
|
||||
ready, _, _ = select.select([self._socket], [], [], 0)
|
||||
try:
|
||||
if ready:
|
||||
ready = self._socket.recv(4096)
|
||||
except OSError as e:
|
||||
# this is for Python 3.x where select.error is a subclass of
|
||||
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||
# https://www.python.org/dev/peps/pep-0475.
|
||||
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
except (select.error, socket.error) as e:
|
||||
# this is for Python 2.x
|
||||
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||
raise SerialException('read failed: {}'.format(e))
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""\
|
||||
Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('ignored reset_output_buffer')
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""\
|
||||
Send break condition. Timed, returns to idle state after given
|
||||
duration.
|
||||
"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('ignored send_break({!r})'.format(duration))
|
||||
|
||||
def _update_break_state(self):
|
||||
"""Set break: Controls TXD. When active, to transmitting is
|
||||
possible."""
|
||||
if self.logger:
|
||||
self.logger.info('ignored _update_break_state({!r})'.format(self._break_state))
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
if self.logger:
|
||||
self.logger.info('ignored _update_rts_state({!r})'.format(self._rts_state))
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
if self.logger:
|
||||
self.logger.info('ignored _update_dtr_state({!r})'.format(self._dtr_state))
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('returning dummy for cts')
|
||||
return True
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('returning dummy for dsr')
|
||||
return True
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('returning dummy for ri')
|
||||
return False
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
if not self.is_open:
|
||||
raise PortNotOpenError()
|
||||
if self.logger:
|
||||
self.logger.info('returning dummy for cd)')
|
||||
return True
|
||||
|
||||
# - - - platform specific - - -
|
||||
|
||||
# works on Linux and probably all the other POSIX systems
|
||||
def fileno(self):
|
||||
"""Get the file handle of the underlying socket for use with select"""
|
||||
return self._socket.fileno()
|
||||
|
||||
|
||||
#
|
||||
# simple client test
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
s = Serial('socket://localhost:7000')
|
||||
sys.stdout.write('{}\n'.format(s))
|
||||
|
||||
sys.stdout.write("write...\n")
|
||||
s.write(b"hello\n")
|
||||
s.flush()
|
||||
sys.stdout.write("read: {}\n".format(s.read(5)))
|
||||
|
||||
s.close()
|
||||
@@ -1,290 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# This module implements a special URL handler that wraps an other port,
|
||||
# print the traffic for debugging purposes. With this, it is possible
|
||||
# to debug the serial port traffic on every application that uses
|
||||
# serial_for_url.
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# URL format: spy://port[?option[=value][&option[=value]]]
|
||||
# options:
|
||||
# - dev=X a file or device to write to
|
||||
# - color use escape code to colorize output
|
||||
# - raw forward raw bytes instead of hexdump
|
||||
#
|
||||
# example:
|
||||
# redirect output to an other terminal window on Posix (Linux):
|
||||
# python -m serial.tools.miniterm spy:///dev/ttyUSB0?dev=/dev/pts/14\&color
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from serial.serialutil import to_bytes
|
||||
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
|
||||
def sixteen(data):
|
||||
"""\
|
||||
yield tuples of hex and ASCII display in multiples of 16. Includes a
|
||||
space after 8 bytes and (None, None) after 16 bytes and at the end.
|
||||
"""
|
||||
n = 0
|
||||
for b in serial.iterbytes(data):
|
||||
yield ('{:02X} '.format(ord(b)), b.decode('ascii') if b' ' <= b < b'\x7f' else '.')
|
||||
n += 1
|
||||
if n == 8:
|
||||
yield (' ', '')
|
||||
elif n >= 16:
|
||||
yield (None, None)
|
||||
n = 0
|
||||
if n > 0:
|
||||
while n < 16:
|
||||
n += 1
|
||||
if n == 8:
|
||||
yield (' ', '')
|
||||
yield (' ', ' ')
|
||||
yield (None, None)
|
||||
|
||||
|
||||
def hexdump(data):
|
||||
"""yield lines with hexdump of data"""
|
||||
values = []
|
||||
ascii = []
|
||||
offset = 0
|
||||
for h, a in sixteen(data):
|
||||
if h is None:
|
||||
yield (offset, ' '.join([''.join(values), ''.join(ascii)]))
|
||||
del values[:]
|
||||
del ascii[:]
|
||||
offset += 0x10
|
||||
else:
|
||||
values.append(h)
|
||||
ascii.append(a)
|
||||
|
||||
|
||||
class FormatRaw(object):
|
||||
"""Forward only RX and TX data to output."""
|
||||
|
||||
def __init__(self, output, color):
|
||||
self.output = output
|
||||
self.color = color
|
||||
self.rx_color = '\x1b[32m'
|
||||
self.tx_color = '\x1b[31m'
|
||||
|
||||
def rx(self, data):
|
||||
"""show received data"""
|
||||
if self.color:
|
||||
self.output.write(self.rx_color)
|
||||
self.output.write(data)
|
||||
self.output.flush()
|
||||
|
||||
def tx(self, data):
|
||||
"""show transmitted data"""
|
||||
if self.color:
|
||||
self.output.write(self.tx_color)
|
||||
self.output.write(data)
|
||||
self.output.flush()
|
||||
|
||||
def control(self, name, value):
|
||||
"""(do not) show control calls"""
|
||||
pass
|
||||
|
||||
|
||||
class FormatHexdump(object):
|
||||
"""\
|
||||
Create a hex dump of RX ad TX data, show when control lines are read or
|
||||
written.
|
||||
|
||||
output example::
|
||||
|
||||
000000.000 Q-RX flushInput
|
||||
000002.469 RTS inactive
|
||||
000002.773 RTS active
|
||||
000003.001 TX 48 45 4C 4C 4F HELLO
|
||||
000003.102 RX 48 45 4C 4C 4F HELLO
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, output, color):
|
||||
self.start_time = time.time()
|
||||
self.output = output
|
||||
self.color = color
|
||||
self.rx_color = '\x1b[32m'
|
||||
self.tx_color = '\x1b[31m'
|
||||
self.control_color = '\x1b[37m'
|
||||
|
||||
def write_line(self, timestamp, label, value, value2=''):
|
||||
self.output.write('{:010.3f} {:4} {}{}\n'.format(timestamp, label, value, value2))
|
||||
self.output.flush()
|
||||
|
||||
def rx(self, data):
|
||||
"""show received data as hex dump"""
|
||||
if self.color:
|
||||
self.output.write(self.rx_color)
|
||||
if data:
|
||||
for offset, row in hexdump(data):
|
||||
self.write_line(time.time() - self.start_time, 'RX', '{:04X} '.format(offset), row)
|
||||
else:
|
||||
self.write_line(time.time() - self.start_time, 'RX', '<empty>')
|
||||
|
||||
def tx(self, data):
|
||||
"""show transmitted data as hex dump"""
|
||||
if self.color:
|
||||
self.output.write(self.tx_color)
|
||||
for offset, row in hexdump(data):
|
||||
self.write_line(time.time() - self.start_time, 'TX', '{:04X} '.format(offset), row)
|
||||
|
||||
def control(self, name, value):
|
||||
"""show control calls"""
|
||||
if self.color:
|
||||
self.output.write(self.control_color)
|
||||
self.write_line(time.time() - self.start_time, name, value)
|
||||
|
||||
|
||||
class Serial(serial.Serial):
|
||||
"""\
|
||||
Inherit the native Serial port implementation and wrap all the methods and
|
||||
attributes.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Serial, self).__init__(*args, **kwargs)
|
||||
self.formatter = None
|
||||
self.show_all = False
|
||||
|
||||
@serial.Serial.port.setter
|
||||
def port(self, value):
|
||||
if value is not None:
|
||||
serial.Serial.port.__set__(self, self.from_url(value))
|
||||
|
||||
def from_url(self, url):
|
||||
"""extract host and port from an URL string"""
|
||||
parts = urlparse.urlsplit(url)
|
||||
if parts.scheme != 'spy':
|
||||
raise serial.SerialException(
|
||||
'expected a string in the form '
|
||||
'"spy://port[?option[=value][&option[=value]]]": '
|
||||
'not starting with spy:// ({!r})'.format(parts.scheme))
|
||||
# process options now, directly altering self
|
||||
formatter = FormatHexdump
|
||||
color = False
|
||||
output = sys.stderr
|
||||
try:
|
||||
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||
if option == 'file':
|
||||
output = open(values[0], 'w')
|
||||
elif option == 'color':
|
||||
color = True
|
||||
elif option == 'raw':
|
||||
formatter = FormatRaw
|
||||
elif option == 'all':
|
||||
self.show_all = True
|
||||
else:
|
||||
raise ValueError('unknown option: {!r}'.format(option))
|
||||
except ValueError as e:
|
||||
raise serial.SerialException(
|
||||
'expected a string in the form '
|
||||
'"spy://port[?option[=value][&option[=value]]]": {}'.format(e))
|
||||
self.formatter = formatter(output, color)
|
||||
return ''.join([parts.netloc, parts.path])
|
||||
|
||||
def write(self, tx):
|
||||
tx = to_bytes(tx)
|
||||
self.formatter.tx(tx)
|
||||
return super(Serial, self).write(tx)
|
||||
|
||||
def read(self, size=1):
|
||||
rx = super(Serial, self).read(size)
|
||||
if rx or self.show_all:
|
||||
self.formatter.rx(rx)
|
||||
return rx
|
||||
|
||||
if hasattr(serial.Serial, 'cancel_read'):
|
||||
def cancel_read(self):
|
||||
self.formatter.control('Q-RX', 'cancel_read')
|
||||
super(Serial, self).cancel_read()
|
||||
|
||||
if hasattr(serial.Serial, 'cancel_write'):
|
||||
def cancel_write(self):
|
||||
self.formatter.control('Q-TX', 'cancel_write')
|
||||
super(Serial, self).cancel_write()
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
n = super(Serial, self).in_waiting
|
||||
if self.show_all:
|
||||
self.formatter.control('Q-RX', 'in_waiting -> {}'.format(n))
|
||||
return n
|
||||
|
||||
def flush(self):
|
||||
self.formatter.control('Q-TX', 'flush')
|
||||
super(Serial, self).flush()
|
||||
|
||||
def reset_input_buffer(self):
|
||||
self.formatter.control('Q-RX', 'reset_input_buffer')
|
||||
super(Serial, self).reset_input_buffer()
|
||||
|
||||
def reset_output_buffer(self):
|
||||
self.formatter.control('Q-TX', 'reset_output_buffer')
|
||||
super(Serial, self).reset_output_buffer()
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
self.formatter.control('BRK', 'send_break {}s'.format(duration))
|
||||
super(Serial, self).send_break(duration)
|
||||
|
||||
@serial.Serial.break_condition.setter
|
||||
def break_condition(self, level):
|
||||
self.formatter.control('BRK', 'active' if level else 'inactive')
|
||||
serial.Serial.break_condition.__set__(self, level)
|
||||
|
||||
@serial.Serial.rts.setter
|
||||
def rts(self, level):
|
||||
self.formatter.control('RTS', 'active' if level else 'inactive')
|
||||
serial.Serial.rts.__set__(self, level)
|
||||
|
||||
@serial.Serial.dtr.setter
|
||||
def dtr(self, level):
|
||||
self.formatter.control('DTR', 'active' if level else 'inactive')
|
||||
serial.Serial.dtr.__set__(self, level)
|
||||
|
||||
@serial.Serial.cts.getter
|
||||
def cts(self):
|
||||
level = super(Serial, self).cts
|
||||
self.formatter.control('CTS', 'active' if level else 'inactive')
|
||||
return level
|
||||
|
||||
@serial.Serial.dsr.getter
|
||||
def dsr(self):
|
||||
level = super(Serial, self).dsr
|
||||
self.formatter.control('DSR', 'active' if level else 'inactive')
|
||||
return level
|
||||
|
||||
@serial.Serial.ri.getter
|
||||
def ri(self):
|
||||
level = super(Serial, self).ri
|
||||
self.formatter.control('RI', 'active' if level else 'inactive')
|
||||
return level
|
||||
|
||||
@serial.Serial.cd.getter
|
||||
def cd(self):
|
||||
level = super(Serial, self).cd
|
||||
self.formatter.control('CD', 'active' if level else 'inactive')
|
||||
return level
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
if __name__ == '__main__':
|
||||
ser = Serial(None)
|
||||
ser.port = 'spy:///dev/ttyS0'
|
||||
print(ser)
|
||||
@@ -1,366 +0,0 @@
|
||||
#! python
|
||||
#
|
||||
# Constants and types for use with Windows API, used by serialwin32.py
|
||||
#
|
||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||
# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# pylint: disable=invalid-name,too-few-public-methods,protected-access,too-many-instance-attributes
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from ctypes import c_ulong, c_void_p, c_int64, c_char, \
|
||||
WinDLL, sizeof, Structure, Union, POINTER
|
||||
from ctypes.wintypes import HANDLE
|
||||
from ctypes.wintypes import BOOL
|
||||
from ctypes.wintypes import LPCWSTR
|
||||
from ctypes.wintypes import DWORD
|
||||
from ctypes.wintypes import WORD
|
||||
from ctypes.wintypes import BYTE
|
||||
_stdcall_libraries = {}
|
||||
_stdcall_libraries['kernel32'] = WinDLL('kernel32')
|
||||
|
||||
INVALID_HANDLE_VALUE = HANDLE(-1).value
|
||||
|
||||
|
||||
# some details of the windows API differ between 32 and 64 bit systems..
|
||||
def is_64bit():
|
||||
"""Returns true when running on a 64 bit system"""
|
||||
return sizeof(c_ulong) != sizeof(c_void_p)
|
||||
|
||||
# ULONG_PTR is a an ordinary number, not a pointer and contrary to the name it
|
||||
# is either 32 or 64 bits, depending on the type of windows...
|
||||
# so test if this a 32 bit windows...
|
||||
if is_64bit():
|
||||
ULONG_PTR = c_int64
|
||||
else:
|
||||
ULONG_PTR = c_ulong
|
||||
|
||||
|
||||
class _SECURITY_ATTRIBUTES(Structure):
|
||||
pass
|
||||
LPSECURITY_ATTRIBUTES = POINTER(_SECURITY_ATTRIBUTES)
|
||||
|
||||
|
||||
try:
|
||||
CreateEventW = _stdcall_libraries['kernel32'].CreateEventW
|
||||
except AttributeError:
|
||||
# Fallback to non wide char version for old OS...
|
||||
from ctypes.wintypes import LPCSTR
|
||||
CreateEventA = _stdcall_libraries['kernel32'].CreateEventA
|
||||
CreateEventA.restype = HANDLE
|
||||
CreateEventA.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCSTR]
|
||||
CreateEvent = CreateEventA
|
||||
|
||||
CreateFileA = _stdcall_libraries['kernel32'].CreateFileA
|
||||
CreateFileA.restype = HANDLE
|
||||
CreateFileA.argtypes = [LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE]
|
||||
CreateFile = CreateFileA
|
||||
else:
|
||||
CreateEventW.restype = HANDLE
|
||||
CreateEventW.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCWSTR]
|
||||
CreateEvent = CreateEventW # alias
|
||||
|
||||
CreateFileW = _stdcall_libraries['kernel32'].CreateFileW
|
||||
CreateFileW.restype = HANDLE
|
||||
CreateFileW.argtypes = [LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE]
|
||||
CreateFile = CreateFileW # alias
|
||||
|
||||
|
||||
class _OVERLAPPED(Structure):
|
||||
pass
|
||||
|
||||
OVERLAPPED = _OVERLAPPED
|
||||
|
||||
|
||||
class _COMSTAT(Structure):
|
||||
pass
|
||||
|
||||
COMSTAT = _COMSTAT
|
||||
|
||||
|
||||
class _DCB(Structure):
|
||||
pass
|
||||
|
||||
DCB = _DCB
|
||||
|
||||
|
||||
class _COMMTIMEOUTS(Structure):
|
||||
pass
|
||||
|
||||
COMMTIMEOUTS = _COMMTIMEOUTS
|
||||
|
||||
GetLastError = _stdcall_libraries['kernel32'].GetLastError
|
||||
GetLastError.restype = DWORD
|
||||
GetLastError.argtypes = []
|
||||
|
||||
LPOVERLAPPED = POINTER(_OVERLAPPED)
|
||||
LPDWORD = POINTER(DWORD)
|
||||
|
||||
GetOverlappedResult = _stdcall_libraries['kernel32'].GetOverlappedResult
|
||||
GetOverlappedResult.restype = BOOL
|
||||
GetOverlappedResult.argtypes = [HANDLE, LPOVERLAPPED, LPDWORD, BOOL]
|
||||
|
||||
ResetEvent = _stdcall_libraries['kernel32'].ResetEvent
|
||||
ResetEvent.restype = BOOL
|
||||
ResetEvent.argtypes = [HANDLE]
|
||||
|
||||
LPCVOID = c_void_p
|
||||
|
||||
WriteFile = _stdcall_libraries['kernel32'].WriteFile
|
||||
WriteFile.restype = BOOL
|
||||
WriteFile.argtypes = [HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED]
|
||||
|
||||
LPVOID = c_void_p
|
||||
|
||||
ReadFile = _stdcall_libraries['kernel32'].ReadFile
|
||||
ReadFile.restype = BOOL
|
||||
ReadFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED]
|
||||
|
||||
CloseHandle = _stdcall_libraries['kernel32'].CloseHandle
|
||||
CloseHandle.restype = BOOL
|
||||
CloseHandle.argtypes = [HANDLE]
|
||||
|
||||
ClearCommBreak = _stdcall_libraries['kernel32'].ClearCommBreak
|
||||
ClearCommBreak.restype = BOOL
|
||||
ClearCommBreak.argtypes = [HANDLE]
|
||||
|
||||
LPCOMSTAT = POINTER(_COMSTAT)
|
||||
|
||||
ClearCommError = _stdcall_libraries['kernel32'].ClearCommError
|
||||
ClearCommError.restype = BOOL
|
||||
ClearCommError.argtypes = [HANDLE, LPDWORD, LPCOMSTAT]
|
||||
|
||||
SetupComm = _stdcall_libraries['kernel32'].SetupComm
|
||||
SetupComm.restype = BOOL
|
||||
SetupComm.argtypes = [HANDLE, DWORD, DWORD]
|
||||
|
||||
EscapeCommFunction = _stdcall_libraries['kernel32'].EscapeCommFunction
|
||||
EscapeCommFunction.restype = BOOL
|
||||
EscapeCommFunction.argtypes = [HANDLE, DWORD]
|
||||
|
||||
GetCommModemStatus = _stdcall_libraries['kernel32'].GetCommModemStatus
|
||||
GetCommModemStatus.restype = BOOL
|
||||
GetCommModemStatus.argtypes = [HANDLE, LPDWORD]
|
||||
|
||||
LPDCB = POINTER(_DCB)
|
||||
|
||||
GetCommState = _stdcall_libraries['kernel32'].GetCommState
|
||||
GetCommState.restype = BOOL
|
||||
GetCommState.argtypes = [HANDLE, LPDCB]
|
||||
|
||||
LPCOMMTIMEOUTS = POINTER(_COMMTIMEOUTS)
|
||||
|
||||
GetCommTimeouts = _stdcall_libraries['kernel32'].GetCommTimeouts
|
||||
GetCommTimeouts.restype = BOOL
|
||||
GetCommTimeouts.argtypes = [HANDLE, LPCOMMTIMEOUTS]
|
||||
|
||||
PurgeComm = _stdcall_libraries['kernel32'].PurgeComm
|
||||
PurgeComm.restype = BOOL
|
||||
PurgeComm.argtypes = [HANDLE, DWORD]
|
||||
|
||||
SetCommBreak = _stdcall_libraries['kernel32'].SetCommBreak
|
||||
SetCommBreak.restype = BOOL
|
||||
SetCommBreak.argtypes = [HANDLE]
|
||||
|
||||
SetCommMask = _stdcall_libraries['kernel32'].SetCommMask
|
||||
SetCommMask.restype = BOOL
|
||||
SetCommMask.argtypes = [HANDLE, DWORD]
|
||||
|
||||
SetCommState = _stdcall_libraries['kernel32'].SetCommState
|
||||
SetCommState.restype = BOOL
|
||||
SetCommState.argtypes = [HANDLE, LPDCB]
|
||||
|
||||
SetCommTimeouts = _stdcall_libraries['kernel32'].SetCommTimeouts
|
||||
SetCommTimeouts.restype = BOOL
|
||||
SetCommTimeouts.argtypes = [HANDLE, LPCOMMTIMEOUTS]
|
||||
|
||||
WaitForSingleObject = _stdcall_libraries['kernel32'].WaitForSingleObject
|
||||
WaitForSingleObject.restype = DWORD
|
||||
WaitForSingleObject.argtypes = [HANDLE, DWORD]
|
||||
|
||||
WaitCommEvent = _stdcall_libraries['kernel32'].WaitCommEvent
|
||||
WaitCommEvent.restype = BOOL
|
||||
WaitCommEvent.argtypes = [HANDLE, LPDWORD, LPOVERLAPPED]
|
||||
|
||||
CancelIoEx = _stdcall_libraries['kernel32'].CancelIoEx
|
||||
CancelIoEx.restype = BOOL
|
||||
CancelIoEx.argtypes = [HANDLE, LPOVERLAPPED]
|
||||
|
||||
ONESTOPBIT = 0 # Variable c_int
|
||||
TWOSTOPBITS = 2 # Variable c_int
|
||||
ONE5STOPBITS = 1
|
||||
|
||||
NOPARITY = 0 # Variable c_int
|
||||
ODDPARITY = 1 # Variable c_int
|
||||
EVENPARITY = 2 # Variable c_int
|
||||
MARKPARITY = 3
|
||||
SPACEPARITY = 4
|
||||
|
||||
RTS_CONTROL_HANDSHAKE = 2 # Variable c_int
|
||||
RTS_CONTROL_DISABLE = 0 # Variable c_int
|
||||
RTS_CONTROL_ENABLE = 1 # Variable c_int
|
||||
RTS_CONTROL_TOGGLE = 3 # Variable c_int
|
||||
SETRTS = 3
|
||||
CLRRTS = 4
|
||||
|
||||
DTR_CONTROL_HANDSHAKE = 2 # Variable c_int
|
||||
DTR_CONTROL_DISABLE = 0 # Variable c_int
|
||||
DTR_CONTROL_ENABLE = 1 # Variable c_int
|
||||
SETDTR = 5
|
||||
CLRDTR = 6
|
||||
|
||||
MS_DSR_ON = 32 # Variable c_ulong
|
||||
EV_RING = 256 # Variable c_int
|
||||
EV_PERR = 512 # Variable c_int
|
||||
EV_ERR = 128 # Variable c_int
|
||||
SETXOFF = 1 # Variable c_int
|
||||
EV_RXCHAR = 1 # Variable c_int
|
||||
GENERIC_WRITE = 1073741824 # Variable c_long
|
||||
PURGE_TXCLEAR = 4 # Variable c_int
|
||||
FILE_FLAG_OVERLAPPED = 1073741824 # Variable c_int
|
||||
EV_DSR = 16 # Variable c_int
|
||||
MAXDWORD = 4294967295 # Variable c_uint
|
||||
EV_RLSD = 32 # Variable c_int
|
||||
|
||||
ERROR_SUCCESS = 0
|
||||
ERROR_NOT_ENOUGH_MEMORY = 8
|
||||
ERROR_OPERATION_ABORTED = 995
|
||||
ERROR_IO_INCOMPLETE = 996
|
||||
ERROR_IO_PENDING = 997 # Variable c_long
|
||||
ERROR_INVALID_USER_BUFFER = 1784
|
||||
|
||||
MS_CTS_ON = 16 # Variable c_ulong
|
||||
EV_EVENT1 = 2048 # Variable c_int
|
||||
EV_RX80FULL = 1024 # Variable c_int
|
||||
PURGE_RXABORT = 2 # Variable c_int
|
||||
FILE_ATTRIBUTE_NORMAL = 128 # Variable c_int
|
||||
PURGE_TXABORT = 1 # Variable c_int
|
||||
SETXON = 2 # Variable c_int
|
||||
OPEN_EXISTING = 3 # Variable c_int
|
||||
MS_RING_ON = 64 # Variable c_ulong
|
||||
EV_TXEMPTY = 4 # Variable c_int
|
||||
EV_RXFLAG = 2 # Variable c_int
|
||||
MS_RLSD_ON = 128 # Variable c_ulong
|
||||
GENERIC_READ = 2147483648 # Variable c_ulong
|
||||
EV_EVENT2 = 4096 # Variable c_int
|
||||
EV_CTS = 8 # Variable c_int
|
||||
EV_BREAK = 64 # Variable c_int
|
||||
PURGE_RXCLEAR = 8 # Variable c_int
|
||||
INFINITE = 0xFFFFFFFF
|
||||
|
||||
CE_RXOVER = 0x0001
|
||||
CE_OVERRUN = 0x0002
|
||||
CE_RXPARITY = 0x0004
|
||||
CE_FRAME = 0x0008
|
||||
CE_BREAK = 0x0010
|
||||
|
||||
|
||||
class N11_OVERLAPPED4DOLLAR_48E(Union):
|
||||
pass
|
||||
|
||||
|
||||
class N11_OVERLAPPED4DOLLAR_484DOLLAR_49E(Structure):
|
||||
pass
|
||||
|
||||
|
||||
N11_OVERLAPPED4DOLLAR_484DOLLAR_49E._fields_ = [
|
||||
('Offset', DWORD),
|
||||
('OffsetHigh', DWORD),
|
||||
]
|
||||
|
||||
PVOID = c_void_p
|
||||
|
||||
N11_OVERLAPPED4DOLLAR_48E._anonymous_ = ['_0']
|
||||
N11_OVERLAPPED4DOLLAR_48E._fields_ = [
|
||||
('_0', N11_OVERLAPPED4DOLLAR_484DOLLAR_49E),
|
||||
('Pointer', PVOID),
|
||||
]
|
||||
_OVERLAPPED._anonymous_ = ['_0']
|
||||
_OVERLAPPED._fields_ = [
|
||||
('Internal', ULONG_PTR),
|
||||
('InternalHigh', ULONG_PTR),
|
||||
('_0', N11_OVERLAPPED4DOLLAR_48E),
|
||||
('hEvent', HANDLE),
|
||||
]
|
||||
_SECURITY_ATTRIBUTES._fields_ = [
|
||||
('nLength', DWORD),
|
||||
('lpSecurityDescriptor', LPVOID),
|
||||
('bInheritHandle', BOOL),
|
||||
]
|
||||
_COMSTAT._fields_ = [
|
||||
('fCtsHold', DWORD, 1),
|
||||
('fDsrHold', DWORD, 1),
|
||||
('fRlsdHold', DWORD, 1),
|
||||
('fXoffHold', DWORD, 1),
|
||||
('fXoffSent', DWORD, 1),
|
||||
('fEof', DWORD, 1),
|
||||
('fTxim', DWORD, 1),
|
||||
('fReserved', DWORD, 25),
|
||||
('cbInQue', DWORD),
|
||||
('cbOutQue', DWORD),
|
||||
]
|
||||
_DCB._fields_ = [
|
||||
('DCBlength', DWORD),
|
||||
('BaudRate', DWORD),
|
||||
('fBinary', DWORD, 1),
|
||||
('fParity', DWORD, 1),
|
||||
('fOutxCtsFlow', DWORD, 1),
|
||||
('fOutxDsrFlow', DWORD, 1),
|
||||
('fDtrControl', DWORD, 2),
|
||||
('fDsrSensitivity', DWORD, 1),
|
||||
('fTXContinueOnXoff', DWORD, 1),
|
||||
('fOutX', DWORD, 1),
|
||||
('fInX', DWORD, 1),
|
||||
('fErrorChar', DWORD, 1),
|
||||
('fNull', DWORD, 1),
|
||||
('fRtsControl', DWORD, 2),
|
||||
('fAbortOnError', DWORD, 1),
|
||||
('fDummy2', DWORD, 17),
|
||||
('wReserved', WORD),
|
||||
('XonLim', WORD),
|
||||
('XoffLim', WORD),
|
||||
('ByteSize', BYTE),
|
||||
('Parity', BYTE),
|
||||
('StopBits', BYTE),
|
||||
('XonChar', c_char),
|
||||
('XoffChar', c_char),
|
||||
('ErrorChar', c_char),
|
||||
('EofChar', c_char),
|
||||
('EvtChar', c_char),
|
||||
('wReserved1', WORD),
|
||||
]
|
||||
_COMMTIMEOUTS._fields_ = [
|
||||
('ReadIntervalTimeout', DWORD),
|
||||
('ReadTotalTimeoutMultiplier', DWORD),
|
||||
('ReadTotalTimeoutConstant', DWORD),
|
||||
('WriteTotalTimeoutMultiplier', DWORD),
|
||||
('WriteTotalTimeoutConstant', DWORD),
|
||||
]
|
||||
__all__ = ['GetLastError', 'MS_CTS_ON', 'FILE_ATTRIBUTE_NORMAL',
|
||||
'DTR_CONTROL_ENABLE', '_COMSTAT', 'MS_RLSD_ON',
|
||||
'GetOverlappedResult', 'SETXON', 'PURGE_TXABORT',
|
||||
'PurgeComm', 'N11_OVERLAPPED4DOLLAR_48E', 'EV_RING',
|
||||
'ONESTOPBIT', 'SETXOFF', 'PURGE_RXABORT', 'GetCommState',
|
||||
'RTS_CONTROL_ENABLE', '_DCB', 'CreateEvent',
|
||||
'_COMMTIMEOUTS', '_SECURITY_ATTRIBUTES', 'EV_DSR',
|
||||
'EV_PERR', 'EV_RXFLAG', 'OPEN_EXISTING', 'DCB',
|
||||
'FILE_FLAG_OVERLAPPED', 'EV_CTS', 'SetupComm',
|
||||
'LPOVERLAPPED', 'EV_TXEMPTY', 'ClearCommBreak',
|
||||
'LPSECURITY_ATTRIBUTES', 'SetCommBreak', 'SetCommTimeouts',
|
||||
'COMMTIMEOUTS', 'ODDPARITY', 'EV_RLSD',
|
||||
'GetCommModemStatus', 'EV_EVENT2', 'PURGE_TXCLEAR',
|
||||
'EV_BREAK', 'EVENPARITY', 'LPCVOID', 'COMSTAT', 'ReadFile',
|
||||
'PVOID', '_OVERLAPPED', 'WriteFile', 'GetCommTimeouts',
|
||||
'ResetEvent', 'EV_RXCHAR', 'LPCOMSTAT', 'ClearCommError',
|
||||
'ERROR_IO_PENDING', 'EscapeCommFunction', 'GENERIC_READ',
|
||||
'RTS_CONTROL_HANDSHAKE', 'OVERLAPPED',
|
||||
'DTR_CONTROL_HANDSHAKE', 'PURGE_RXCLEAR', 'GENERIC_WRITE',
|
||||
'LPDCB', 'CreateEventW', 'SetCommMask', 'EV_EVENT1',
|
||||
'SetCommState', 'LPVOID', 'CreateFileW', 'LPDWORD',
|
||||
'EV_RX80FULL', 'TWOSTOPBITS', 'LPCOMMTIMEOUTS', 'MAXDWORD',
|
||||
'MS_DSR_ON', 'MS_RING_ON',
|
||||
'N11_OVERLAPPED4DOLLAR_484DOLLAR_49E', 'EV_ERR',
|
||||
'ULONG_PTR', 'CreateFile', 'NOPARITY', 'CloseHandle']
|
||||
Reference in New Issue
Block a user