Skip to content

Commit b4a72dc

Browse files
authored
Use console_scripts for CLI (#133)
* use console_scripts to build cross-platform scripts (`udapy`) at install time. * keep `bin/udapy` for backward compatibility.
1 parent 180f6f1 commit b4a72dc

File tree

4 files changed

+150
-137
lines changed

4 files changed

+150
-137
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.cache
12
.idea
3+
*.egg-info/
24
*.pyc
3-
.cache
5+
dist/

bin/udapy

Lines changed: 3 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,7 @@
11
#!/usr/bin/env python3
2-
import os
3-
import gc
2+
"""Thin wrapper for backward compatibility. Calls udapi.cli.main()."""
43
import sys
5-
import atexit
6-
import logging
7-
import argparse
4+
from udapi.cli import main
85

9-
from udapi.core.run import Run
10-
11-
# Parse command line arguments.
12-
argparser = argparse.ArgumentParser(
13-
formatter_class=argparse.RawTextHelpFormatter,
14-
usage="udapy [optional_arguments] scenario",
15-
epilog="See http://udapi.github.io",
16-
description="udapy - Python interface to Udapi - API for Universal Dependencies\n\n"
17-
"Examples of usage:\n"
18-
" udapy -s read.Sentences udpipe.En < in.txt > out.conllu\n"
19-
" udapy -T < sample.conllu | less -R\n"
20-
" udapy -HAM ud.MarkBugs < sample.conllu > bugs.html\n")
21-
argparser.add_argument(
22-
"-q", "--quiet", action="store_true",
23-
help="Warning, info and debug messages are suppressed. Only fatal errors are reported.")
24-
argparser.add_argument(
25-
"-v", "--verbose", action="store_true",
26-
help="Warning, info and debug messages are printed to the STDERR.")
27-
argparser.add_argument(
28-
"-s", "--save", action="store_true",
29-
help="Add write.Conllu to the end of the scenario")
30-
argparser.add_argument(
31-
"-T", "--save_text_mode_trees", action="store_true",
32-
help="Add write.TextModeTrees color=1 to the end of the scenario")
33-
argparser.add_argument(
34-
"-H", "--save_html", action="store_true",
35-
help="Add write.TextModeTreesHtml color=1 to the end of the scenario")
36-
argparser.add_argument(
37-
"-A", "--save_all_attributes", action="store_true",
38-
help="Add attributes=form,lemma,upos,xpos,feats,deprel,misc (to be used after -T and -H)")
39-
argparser.add_argument(
40-
"-C", "--save_comments", action="store_true",
41-
help="Add print_comments=1 (to be used after -T and -H)")
42-
argparser.add_argument(
43-
"-M", "--marked_only", action="store_true",
44-
help="Add marked_only=1 to the end of the scenario (to be used after -T and -H)")
45-
argparser.add_argument(
46-
"-N", "--no_color", action="store_true",
47-
help="Add color=0 to the end of the scenario, this overrides color=1 of -T and -H")
48-
argparser.add_argument(
49-
"-X", "--extra", action="append",
50-
help="Add a specified parameter (or a block name) to the end of the scenario\n"
51-
"For example 'udapy -TNX attributes=form,misc -X layout=align < my.conllu'")
52-
argparser.add_argument(
53-
"--gc", action="store_true",
54-
help="By default, udapy disables Python garbage collection and at-exit cleanup\n"
55-
"to speed up everything (especially reading CoNLL-U files). In edge cases,\n"
56-
"when processing many files and running out of memory, you can disable this\n"
57-
"optimization (i.e. enable garbage collection) with 'udapy --gc'.")
58-
argparser.add_argument(
59-
'scenario', nargs=argparse.REMAINDER, help="A sequence of blocks and their parameters.")
60-
61-
args = argparser.parse_args()
62-
63-
# Set the level of logs according to parameters.
64-
if args.verbose:
65-
level = logging.DEBUG
66-
elif args.quiet:
67-
level = logging.CRITICAL
68-
else:
69-
level = logging.INFO
70-
71-
logging.basicConfig(format='%(asctime)-15s [%(levelname)7s] %(funcName)s - %(message)s',
72-
level=level)
73-
74-
# Global flag to track if an unhandled exception occurred
75-
_unhandled_exception_occurred = False
76-
77-
def _custom_excepthook(exc_type, exc_value, traceback):
78-
global _unhandled_exception_occurred
79-
_unhandled_exception_occurred = True
80-
81-
# Call the default excepthook to allow normal error reporting
82-
sys.__excepthook__(exc_type, exc_value, traceback)
83-
84-
# Override the default excepthook
85-
sys.excepthook = _custom_excepthook
86-
87-
88-
# Process and provide the scenario.
896
if __name__ == "__main__":
90-
91-
# Disabling garbage collections makes the whole processing much faster.
92-
# Similarly, we can save several seconds by partially disabling the at-exit Python cleanup
93-
# (atexit hooks are called in reversed order of their registration,
94-
# so flushing stdio buffers etc. will be still done before the os._exit(0) call).
95-
# See https://instagram-engineering.com/dismissing-python-garbage-collection-at-instagram-4dca40b29172
96-
# Is it safe to disable GC?
97-
# OS will free the memory allocated by this process after it ends anyway.
98-
# The udapy wrapper is aimed for one-time tasks, not a long-running server,
99-
# so in a typical case a document is loaded and almost no memory is freed before the end.
100-
# Udapi documents have a many cyclic references, so running GC is quite slow.
101-
if not args.gc:
102-
gc.disable()
103-
# When an exception/error has happened, udapy should exit with a non-zero exit code,
104-
# so that users can use `udapy ... || echo "Error detected"` (or Makefile reports errors).
105-
# However, we cannot use `atexit.register(lambda: os._exit(1 if sys.exc_info()[0] else 0))`
106-
# because the Python has already exited the exception-handling block
107-
# (the exception/error has been already reported and sys.exc_info()[0] is None).
108-
# We thus keep record whether _unhandled_exception_occurred.
109-
atexit.register(lambda: os._exit(1 if _unhandled_exception_occurred else 0))
110-
atexit.register(sys.stderr.flush)
111-
if args.save:
112-
args.scenario = args.scenario + ['write.Conllu']
113-
if args.save_text_mode_trees:
114-
args.scenario = args.scenario + ['write.TextModeTrees', 'color=1']
115-
if args.save_html:
116-
args.scenario = args.scenario + ['write.TextModeTreesHtml', 'color=1']
117-
if args.save_all_attributes:
118-
args.scenario = args.scenario + ['attributes=form,lemma,upos,xpos,feats,deprel,misc']
119-
if args.save_comments:
120-
args.scenario = args.scenario + ['print_comments=1']
121-
if args.marked_only:
122-
args.scenario = args.scenario + ['marked_only=1']
123-
if args.no_color:
124-
args.scenario = args.scenario + ['color=0']
125-
if args.extra:
126-
args.scenario += args.extra
127-
128-
runner = Run(args)
129-
# udapy is often piped to head etc., e.g.
130-
# `seq 1000 | udapy -s read.Sentences | head`
131-
# Let's prevent Python from reporting (with distracting stacktrace)
132-
# "BrokenPipeError: [Errno 32] Broken pipe"
133-
try:
134-
runner.execute()
135-
except BrokenPipeError:
136-
pass
7+
sys.exit(main())

setup.cfg

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ classifiers =
1616
packages = find:
1717
python_requires = >=3.9
1818
include_package_data = True
19-
scripts =
20-
bin/udapy
2119
install_requires =
2220
colorama
2321
termcolor
2422

23+
[options.entry_points]
24+
console_scripts =
25+
udapy = udapi:cli.main
26+
2527
[options.extras_require]
2628
test =
2729
pytest
28-
29-

udapi/cli.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import gc
4+
import sys
5+
import atexit
6+
import logging
7+
import argparse
8+
9+
from udapi.core.run import Run
10+
11+
# Parse command line arguments.
12+
argparser = argparse.ArgumentParser(
13+
formatter_class=argparse.RawTextHelpFormatter,
14+
usage="udapy [optional_arguments] scenario",
15+
epilog="See http://udapi.github.io",
16+
description="udapy - Python interface to Udapi - API for Universal Dependencies\n\n"
17+
"Examples of usage:\n"
18+
" udapy -s read.Sentences udpipe.En < in.txt > out.conllu\n"
19+
" udapy -T < sample.conllu | less -R\n"
20+
" udapy -HAM ud.MarkBugs < sample.conllu > bugs.html\n")
21+
argparser.add_argument(
22+
"-q", "--quiet", action="store_true",
23+
help="Warning, info and debug messages are suppressed. Only fatal errors are reported.")
24+
argparser.add_argument(
25+
"-v", "--verbose", action="store_true",
26+
help="Warning, info and debug messages are printed to the STDERR.")
27+
argparser.add_argument(
28+
"-s", "--save", action="store_true",
29+
help="Add write.Conllu to the end of the scenario")
30+
argparser.add_argument(
31+
"-T", "--save_text_mode_trees", action="store_true",
32+
help="Add write.TextModeTrees color=1 to the end of the scenario")
33+
argparser.add_argument(
34+
"-H", "--save_html", action="store_true",
35+
help="Add write.TextModeTreesHtml color=1 to the end of the scenario")
36+
argparser.add_argument(
37+
"-A", "--save_all_attributes", action="store_true",
38+
help="Add attributes=form,lemma,upos,xpos,feats,deprel,misc (to be used after -T and -H)")
39+
argparser.add_argument(
40+
"-C", "--save_comments", action="store_true",
41+
help="Add print_comments=1 (to be used after -T and -H)")
42+
argparser.add_argument(
43+
"-M", "--marked_only", action="store_true",
44+
help="Add marked_only=1 to the end of the scenario (to be used after -T and -H)")
45+
argparser.add_argument(
46+
"-N", "--no_color", action="store_true",
47+
help="Add color=0 to the end of the scenario, this overrides color=1 of -T and -H")
48+
argparser.add_argument(
49+
"-X", "--extra", action="append",
50+
help="Add a specified parameter (or a block name) to the end of the scenario\n"
51+
"For example 'udapy -TNX attributes=form,misc -X layout=align < my.conllu'")
52+
argparser.add_argument(
53+
"--gc", action="store_true",
54+
help="By default, udapy disables Python garbage collection and at-exit cleanup\n"
55+
"to speed up everything (especially reading CoNLL-U files). In edge cases,\n"
56+
"when processing many files and running out of memory, you can disable this\n"
57+
"optimization (i.e. enable garbage collection) with 'udapy --gc'.")
58+
argparser.add_argument(
59+
'scenario', nargs=argparse.REMAINDER, help="A sequence of blocks and their parameters.")
60+
61+
62+
# Process and provide the scenario.
63+
def main(argv=None):
64+
args = argparser.parse_args(argv)
65+
66+
# Set the level of logs according to parameters.
67+
if args.verbose:
68+
level = logging.DEBUG
69+
elif args.quiet:
70+
level = logging.CRITICAL
71+
else:
72+
level = logging.INFO
73+
74+
logging.basicConfig(format='%(asctime)-15s [%(levelname)7s] %(funcName)s - %(message)s',
75+
level=level)
76+
77+
# Global flag to track if an unhandled exception occurred
78+
_unhandled_exception_occurred = False
79+
80+
def _custom_excepthook(exc_type, exc_value, traceback):
81+
global _unhandled_exception_occurred
82+
_unhandled_exception_occurred = True
83+
84+
# Call the default excepthook to allow normal error reporting
85+
sys.__excepthook__(exc_type, exc_value, traceback)
86+
87+
# Override the default excepthook
88+
sys.excepthook = _custom_excepthook
89+
90+
# Disabling garbage collections makes the whole processing much faster.
91+
# Similarly, we can save several seconds by partially disabling the at-exit Python cleanup
92+
# (atexit hooks are called in reversed order of their registration,
93+
# so flushing stdio buffers etc. will be still done before the os._exit(0) call).
94+
# See https://instagram-engineering.com/dismissing-python-garbage-collection-at-instagram-4dca40b29172
95+
# Is it safe to disable GC?
96+
# OS will free the memory allocated by this process after it ends anyway.
97+
# The udapy wrapper is aimed for one-time tasks, not a long-running server,
98+
# so in a typical case a document is loaded and almost no memory is freed before the end.
99+
# Udapi documents have a many cyclic references, so running GC is quite slow.
100+
if not args.gc:
101+
gc.disable()
102+
# When an exception/error has happened, udapy should exit with a non-zero exit code,
103+
# so that users can use `udapy ... || echo "Error detected"` (or Makefile reports errors).
104+
# However, we cannot use `atexit.register(lambda: os._exit(1 if sys.exc_info()[0] else 0))`
105+
# because the Python has already exited the exception-handling block
106+
# (the exception/error has been already reported and sys.exc_info()[0] is None).
107+
# We thus keep record whether _unhandled_exception_occurred.
108+
atexit.register(lambda: os._exit(1 if _unhandled_exception_occurred else 0))
109+
atexit.register(sys.stderr.flush)
110+
if args.save:
111+
args.scenario = args.scenario + ['write.Conllu']
112+
if args.save_text_mode_trees:
113+
args.scenario = args.scenario + ['write.TextModeTrees', 'color=1']
114+
if args.save_html:
115+
args.scenario = args.scenario + ['write.TextModeTreesHtml', 'color=1']
116+
if args.save_all_attributes:
117+
args.scenario = args.scenario + ['attributes=form,lemma,upos,xpos,feats,deprel,misc']
118+
if args.save_comments:
119+
args.scenario = args.scenario + ['print_comments=1']
120+
if args.marked_only:
121+
args.scenario = args.scenario + ['marked_only=1']
122+
if args.no_color:
123+
args.scenario = args.scenario + ['color=0']
124+
if args.extra:
125+
args.scenario += args.extra
126+
127+
runner = Run(args)
128+
# udapy is often piped to head etc., e.g.
129+
# `seq 1000 | udapy -s read.Sentences | head`
130+
# Let's prevent Python from reporting (with distracting stacktrace)
131+
# "BrokenPipeError: [Errno 32] Broken pipe"
132+
try:
133+
runner.execute()
134+
except BrokenPipeError:
135+
pass
136+
return 0
137+
138+
139+
if __name__ == "__main__":
140+
sys.exit(main())

0 commit comments

Comments
 (0)