ncrystal-python 3.9.81__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- NCrystal/__init__.py +85 -0
- NCrystal/__main__.py +98 -0
- NCrystal/_chooks.py +854 -0
- NCrystal/_cli_cif2ncmat.py +269 -0
- NCrystal/_cli_endf2ncmat.py +503 -0
- NCrystal/_cli_hfg2ncmat.py +144 -0
- NCrystal/_cli_mcstasunion.py +74 -0
- NCrystal/_cli_ncmat2cpp.py +31 -0
- NCrystal/_cli_ncmat2hkl.py +180 -0
- NCrystal/_cli_nctool.py +1018 -0
- NCrystal/_cli_vdos2ncmat.py +463 -0
- NCrystal/_cli_verifyatompos.py +257 -0
- NCrystal/_cliimpl.py +307 -0
- NCrystal/_cliwrap_config.py +36 -0
- NCrystal/_common.py +499 -0
- NCrystal/_coreimpl.py +114 -0
- NCrystal/_hfgdata.py +546 -0
- NCrystal/_hklobjects.py +136 -0
- NCrystal/_is_std.py +0 -0
- NCrystal/_locatelib.py +210 -0
- NCrystal/_miscimpl.py +354 -0
- NCrystal/_mmc.py +757 -0
- NCrystal/_msg.py +60 -0
- NCrystal/_ncmat2cpp_impl.py +445 -0
- NCrystal/_ncmatimpl.py +2131 -0
- NCrystal/_numpy.py +76 -0
- NCrystal/_testimpl.py +579 -0
- NCrystal/api.py +56 -0
- NCrystal/atomdata.py +177 -0
- NCrystal/cfgstr.py +77 -0
- NCrystal/cifutils.py +1795 -0
- NCrystal/cli.py +96 -0
- NCrystal/constants.py +134 -0
- NCrystal/core.py +1910 -0
- NCrystal/datasrc.py +226 -0
- NCrystal/exceptions.py +66 -0
- NCrystal/hfg2ncmat.py +270 -0
- NCrystal/mcstasutils.py +438 -0
- NCrystal/misc.py +317 -0
- NCrystal/mmc.py +35 -0
- NCrystal/ncmat.py +778 -0
- NCrystal/ncmat2cpp.py +80 -0
- NCrystal/obsolete.py +67 -0
- NCrystal/plot.py +484 -0
- NCrystal/plugins.py +49 -0
- NCrystal/test.py +76 -0
- NCrystal/vdos.py +1034 -0
- ncrystal_python-3.9.81.dist-info/LICENSE +206 -0
- ncrystal_python-3.9.81.dist-info/METADATA +515 -0
- ncrystal_python-3.9.81.dist-info/RECORD +53 -0
- ncrystal_python-3.9.81.dist-info/WHEEL +5 -0
- ncrystal_python-3.9.81.dist-info/entry_points.txt +10 -0
- ncrystal_python-3.9.81.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
|
|
2
|
+
################################################################################
|
|
3
|
+
## ##
|
|
4
|
+
## This file is part of NCrystal (see https://mctools.github.io/ncrystal/) ##
|
|
5
|
+
## ##
|
|
6
|
+
## Copyright 2015-2024 NCrystal developers ##
|
|
7
|
+
## ##
|
|
8
|
+
## Licensed under the Apache License, Version 2.0 (the "License"); ##
|
|
9
|
+
## you may not use this file except in compliance with the License. ##
|
|
10
|
+
## You may obtain a copy of the License at ##
|
|
11
|
+
## ##
|
|
12
|
+
## http://www.apache.org/licenses/LICENSE-2.0 ##
|
|
13
|
+
## ##
|
|
14
|
+
## Unless required by applicable law or agreed to in writing, software ##
|
|
15
|
+
## distributed under the License is distributed on an "AS IS" BASIS, ##
|
|
16
|
+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ##
|
|
17
|
+
## See the License for the specific language governing permissions and ##
|
|
18
|
+
## limitations under the License. ##
|
|
19
|
+
## ##
|
|
20
|
+
################################################################################
|
|
21
|
+
|
|
22
|
+
from ._cliimpl import ( create_ArgumentParser,
|
|
23
|
+
cli_entry_point,
|
|
24
|
+
print )
|
|
25
|
+
import pathlib
|
|
26
|
+
|
|
27
|
+
def parseArgs( progname, args, return_parser=False ):
|
|
28
|
+
|
|
29
|
+
descr="""
|
|
30
|
+
Load input file (for instance an .ncmat file) with NCrystal and verify that the
|
|
31
|
+
atom-positions are compatible with the indicated space group. Input must be in a
|
|
32
|
+
format supported by NCrystal (e.g. .ncmat), and must represent a crystalline
|
|
33
|
+
material with space group and atom position information. And exit code of 0
|
|
34
|
+
indicates no issues found with atom positions, exit code of 1 indicates issues,
|
|
35
|
+
and exit code of 99 indicates that the material is not crystalline with space
|
|
36
|
+
group and atom positions.
|
|
37
|
+
|
|
38
|
+
The symmetry checking is internally performed with the help of the ASE[1]
|
|
39
|
+
spacegroup module, so it might be necessary to install this module first with:
|
|
40
|
+
|
|
41
|
+
python3 -mpip install ase
|
|
42
|
+
|
|
43
|
+
References:
|
|
44
|
+
[1]: Ask Hjorth Larsen et al 2017 J. Phys.: Condens. Matter 29 27300
|
|
45
|
+
https://doi.org/10.1088/1361-648X/aa680e
|
|
46
|
+
"""
|
|
47
|
+
parser = create_ArgumentParser(prog = progname,
|
|
48
|
+
description=descr)
|
|
49
|
+
parser.add_argument("FILE",help="File to load")
|
|
50
|
+
parser.add_argument("--quiet",default=False,action='store_true',
|
|
51
|
+
help="Will not produce any output unless errors are found")
|
|
52
|
+
parser.add_argument("--wyckoff",default=False,action='store_true',
|
|
53
|
+
help='Output just Wyckoff positions in easily parsable format (implies --quiet, except for the Wyckoff positions).')
|
|
54
|
+
def_eps = 1e-6
|
|
55
|
+
parser.add_argument("--epsilon",default=def_eps,type=float,
|
|
56
|
+
help="Level of precision required")
|
|
57
|
+
|
|
58
|
+
if return_parser:
|
|
59
|
+
return parser
|
|
60
|
+
args = parser.parse_args(args)
|
|
61
|
+
if args.wyckoff:
|
|
62
|
+
args.quiet = True
|
|
63
|
+
return args
|
|
64
|
+
|
|
65
|
+
def create_argparser_for_sphinx( progname ):
|
|
66
|
+
return parseArgs(progname,[],return_parser=True)
|
|
67
|
+
|
|
68
|
+
@cli_entry_point
|
|
69
|
+
def main( progname, args ):
|
|
70
|
+
args = parseArgs( progname, args )
|
|
71
|
+
from ._common import get_ncrystal_print_fct
|
|
72
|
+
wyckoff_print = get_ncrystal_print_fct() if args.wyckoff else None
|
|
73
|
+
if args.quiet:
|
|
74
|
+
from ._common import modify_ncrystal_print_fct_ctxmgr
|
|
75
|
+
with modify_ncrystal_print_fct_ctxmgr('block'):
|
|
76
|
+
_main_impl(args,wyckoff_print=wyckoff_print)
|
|
77
|
+
else:
|
|
78
|
+
_main_impl(args)
|
|
79
|
+
|
|
80
|
+
def _main_impl( args, wyckoff_print = None ):
|
|
81
|
+
|
|
82
|
+
if wyckoff_print is None:
|
|
83
|
+
wyckoff_print = print
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
import ase.spacegroup
|
|
87
|
+
except ImportError as e:
|
|
88
|
+
raise RuntimeError('Could not import ase.spacegroup. Depending on your'
|
|
89
|
+
' environment, this can be installed with'
|
|
90
|
+
' "python3 -mpip install ase" or '
|
|
91
|
+
'"conda install -c conda-forge ase".') from e
|
|
92
|
+
#Load data:
|
|
93
|
+
from .core import createInfo
|
|
94
|
+
info = createInfo(args.FILE)
|
|
95
|
+
print(f'Loaded info based on: "{args.FILE}"')
|
|
96
|
+
if info.isMultiPhase():
|
|
97
|
+
raise RuntimeError('Not applicable: Material has multiple phases')
|
|
98
|
+
if not info.atominfos:
|
|
99
|
+
raise RuntimeError('Not applicable: Material does not have atom positions')
|
|
100
|
+
if not info.hasStructureInfo() or not info.getStructureInfo()['spacegroup']:
|
|
101
|
+
raise RuntimeError('Not applicable: Material does not have unit cell structure info')
|
|
102
|
+
sg_no = info.getStructureInfo()['spacegroup']
|
|
103
|
+
|
|
104
|
+
#Verify filename is not misleading:
|
|
105
|
+
for fnpart in pathlib.Path(args.FILE).stem.split('_'):
|
|
106
|
+
for pattern in ('sg','spacegroup','sgno','spacegr','spacegrp'):
|
|
107
|
+
if fnpart.lower().startswith('sg'):
|
|
108
|
+
_ = fnpart[2:]
|
|
109
|
+
if _ and _.isdigit() and int(_)!=sg_no:
|
|
110
|
+
raise RuntimeError(f"Filename indicates a different spacegroup number ({int(_)}) than the one specified in the data ({sg_no}).")
|
|
111
|
+
# Analysis utils:
|
|
112
|
+
from ._numpy import (_ensure_numpy, _np )
|
|
113
|
+
_ensure_numpy()
|
|
114
|
+
np = _np
|
|
115
|
+
_cell_offsets = np.asarray(list((a,b,c) for a in (-1,0,1) for b in (-1,0,1) for c in (-1,0,1)),dtype=float)
|
|
116
|
+
class UnitCellPoint:
|
|
117
|
+
def __init__(self,xyz):
|
|
118
|
+
"""Unit cell point, all coordinates will be integrally shifted to have values in [0,1)"""
|
|
119
|
+
self._pt = np.asarray(xyz,dtype=float)
|
|
120
|
+
assert len(self._pt)==3
|
|
121
|
+
for i in range(3):
|
|
122
|
+
while xyz[i]<0.0:
|
|
123
|
+
xyz+=1.0
|
|
124
|
+
while xyz[i]>=1.0:
|
|
125
|
+
xyz-=1.0
|
|
126
|
+
self._pt = np.asarray(xyz,dtype=float)
|
|
127
|
+
self._mperiodicpts = -(self._pt + _cell_offsets)
|
|
128
|
+
|
|
129
|
+
def distSq(self,other):
|
|
130
|
+
"""Calculate distance-squared to another unit cell point, taking into account
|
|
131
|
+
the periodicity of the lattice (i.e. points at (0.001,0.001,0.001) and
|
|
132
|
+
(0.999,0.999,0.999) are actually very close to each other)."""
|
|
133
|
+
return np.sum(( self._mperiodicpts + other._pt )**2,axis=1).min()
|
|
134
|
+
|
|
135
|
+
def __str__(self):
|
|
136
|
+
return str(self._pt)
|
|
137
|
+
|
|
138
|
+
def fmt(self,ndec = 7):
|
|
139
|
+
return (f'(%.{ndec}g, %.{ndec}g, %.{ndec}g)')%tuple(self._pt)
|
|
140
|
+
|
|
141
|
+
def matchUpPoints(pts1,pts2):
|
|
142
|
+
"""Match up points in the two lists of UnitCellPoint's, finding those of
|
|
143
|
+
closest distance. Returns matches,leftovers where matches is a list of
|
|
144
|
+
(pt1,pt2,dist), and leftovers is a list of unmatched points in case lists
|
|
145
|
+
are of unequal length. The argument lists will be consumed.
|
|
146
|
+
"""
|
|
147
|
+
n1 = len(pts1)
|
|
148
|
+
n2 = len(pts2)
|
|
149
|
+
assert n1 > 0 and n2 > 0
|
|
150
|
+
assert isinstance(pts1[0],UnitCellPoint) and isinstance(pts2[0],UnitCellPoint)
|
|
151
|
+
#eps=0.001
|
|
152
|
+
|
|
153
|
+
#Create a matrix of distances between pts in the two lists:
|
|
154
|
+
dists = np.empty( shape=(n1,n2) )
|
|
155
|
+
for i1,p1 in enumerate(pts1):
|
|
156
|
+
for i2,p2 in enumerate(pts2):
|
|
157
|
+
dists[i1,i2] = p1.distSq(p2)
|
|
158
|
+
|
|
159
|
+
#Keep "popping" the pt pairs with the smallest distances out of the data
|
|
160
|
+
#structures, while recording them in the matches list:
|
|
161
|
+
matches = []
|
|
162
|
+
for i in range(min(n1,n2)):
|
|
163
|
+
idx = np.argmin(dists)
|
|
164
|
+
i2, i1 = idx%len(pts2), idx//len(pts2)
|
|
165
|
+
thedist = np.sqrt(dists[i1,i2])
|
|
166
|
+
#delete entries for these pts and record in list of matches:
|
|
167
|
+
if dists.shape[0]>0:
|
|
168
|
+
dists = np.delete(dists, (i1), axis=0)
|
|
169
|
+
if dists.shape[1]>0:
|
|
170
|
+
dists = np.delete(dists, (i2), axis=1)
|
|
171
|
+
p1 = pts1.pop(i1)
|
|
172
|
+
p2 = pts2.pop(i2)
|
|
173
|
+
matches.append( ( p1, p2, thedist ) )
|
|
174
|
+
assert not ( pts1 and pts2 )#one or both should be empty
|
|
175
|
+
return matches, pts1 or pts2 or []
|
|
176
|
+
|
|
177
|
+
def analyse(info, spacegroup,sgdescr):
|
|
178
|
+
"""Loop over atoms and verify/analyse. Returns False in case of issues"""
|
|
179
|
+
sg=spacegroup
|
|
180
|
+
errors = False
|
|
181
|
+
wyckoff=[]
|
|
182
|
+
|
|
183
|
+
matchlvl = args.epsilon
|
|
184
|
+
nprintdecs = max(7,1+int(np.ceil(-np.log10(matchlvl))))
|
|
185
|
+
|
|
186
|
+
distmax = 0.0
|
|
187
|
+
for ai in info.atominfos:
|
|
188
|
+
lbl=ai.atomData.displayLabel()
|
|
189
|
+
print(f"\n==========================> Investigating element: {lbl}")
|
|
190
|
+
print(f"\nThe {len(ai.positions)} positions are generated from the following symmetry-unique points:")
|
|
191
|
+
unique_sites = list(e for e in sg.unique_sites(ai.positions))
|
|
192
|
+
tags = sg.tag_sites(unique_sites + list(ai.positions))
|
|
193
|
+
|
|
194
|
+
for idx,us in zip(tags[0:len(unique_sites)],unique_sites):
|
|
195
|
+
wyckoff.append( ( lbl, tuple(us) ) )
|
|
196
|
+
print(f"\nSymmetry-unique point: {us}")
|
|
197
|
+
#equiv_sites = sg.equivalent_sites([us])[0]
|
|
198
|
+
expected_sites = list(UnitCellPoint(e) for e in sg.equivalent_sites([us])[0])
|
|
199
|
+
actual_sites = list(UnitCellPoint(p) for i,p in enumerate(ai.positions) if tags[len(unique_sites)+i]==idx)
|
|
200
|
+
matches,leftovers = matchUpPoints( expected_sites, actual_sites )
|
|
201
|
+
for p1,p2,dist in matches:
|
|
202
|
+
distmax = max(dist,distmax)
|
|
203
|
+
problem= bool(dist>matchlvl)
|
|
204
|
+
errors = errors or problem
|
|
205
|
+
expected_str = p1.fmt(nprintdecs)
|
|
206
|
+
actual_str = p2.fmt(nprintdecs)
|
|
207
|
+
problem_str = ' <-- PROBLEM!!!' if problem else ''
|
|
208
|
+
expected_str = '' if expected_str==actual_str else ' '+expected_str
|
|
209
|
+
print(f" -> Point {actual_str} deviates {dist:g} from expected position{expected_str}{problem_str}")
|
|
210
|
+
if leftovers:
|
|
211
|
+
errors = True
|
|
212
|
+
if len(expected_sites)>len(actual_sites):
|
|
213
|
+
print("\n ERROR: The following expected symmetry-calculated points were absent from the actual list of points:")
|
|
214
|
+
else:
|
|
215
|
+
print("\n ERROR: The following points were not found among the symmetry-calculated points:")
|
|
216
|
+
for p in leftovers:
|
|
217
|
+
print(' -> '+p.fmt(nprintdecs))
|
|
218
|
+
|
|
219
|
+
print("\n==========================> Done.\n")
|
|
220
|
+
if not errors:
|
|
221
|
+
print(f"All OK! Atom positions are from these Wyckoff positions for {sgdescr}:\n")
|
|
222
|
+
if args.wyckoff:
|
|
223
|
+
wyckoff_print(f'# Wyckoff positions for {sgdescr}:')
|
|
224
|
+
def fmt( x ):
|
|
225
|
+
return (f'%.{nprintdecs}g')%x
|
|
226
|
+
for lbl,pos in wyckoff:
|
|
227
|
+
wyckoff_print(f'{" " if not args.wyckoff else ""}{lbl} {fmt(pos[0])} {fmt(pos[1])} {fmt(pos[2])}')
|
|
228
|
+
print(f'\nWorst discrepancy in distances: {distmax:.3g}')
|
|
229
|
+
return True
|
|
230
|
+
else:
|
|
231
|
+
return False
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
sg_setting1 = ase.spacegroup.Spacegroup(sg_no,setting=1)
|
|
235
|
+
print(f"Input has space group: {sg_no} ({sg_setting1.symbol})")
|
|
236
|
+
|
|
237
|
+
#Some space-groups support an alternate setting (different point of origin?):
|
|
238
|
+
try:
|
|
239
|
+
sg_setting2 = ase.spacegroup.Spacegroup(sg_no,setting=2)
|
|
240
|
+
except ase.spacegroup.spacegroup.SpacegroupNotFoundError:
|
|
241
|
+
sg_setting2 = None
|
|
242
|
+
|
|
243
|
+
assert sg_setting1 is not sg_setting2
|
|
244
|
+
|
|
245
|
+
if analyse(info, sg_setting1, sgdescr=f'SG {sg_no} (setting=1)'):
|
|
246
|
+
return # all ok
|
|
247
|
+
problem_msg = ('Problems detected in list of atom positions! Most likely'
|
|
248
|
+
' this is a real problem, but you can also check with'
|
|
249
|
+
f' (enter spacegroup {sg_no}): '
|
|
250
|
+
'https://www.cryst.ehu.es/cryst/get_wp.html')
|
|
251
|
+
if sg_setting2 is None:
|
|
252
|
+
raise RuntimeError(problem_msg)
|
|
253
|
+
print('\n\nProblems encountered. Re-trying with alternative'
|
|
254
|
+
' space-group setting (ase.spacegroup.Spacegroup(setting=2))\n\n')
|
|
255
|
+
if analyse(info, sg_setting2, sgdescr=f'SG {sg_no} (setting=2)'):
|
|
256
|
+
return # all ok
|
|
257
|
+
raise RuntimeError(problem_msg)
|
NCrystal/_cliimpl.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
|
|
2
|
+
################################################################################
|
|
3
|
+
## ##
|
|
4
|
+
## This file is part of NCrystal (see https://mctools.github.io/ncrystal/) ##
|
|
5
|
+
## ##
|
|
6
|
+
## Copyright 2015-2024 NCrystal developers ##
|
|
7
|
+
## ##
|
|
8
|
+
## Licensed under the Apache License, Version 2.0 (the "License"); ##
|
|
9
|
+
## you may not use this file except in compliance with the License. ##
|
|
10
|
+
## You may obtain a copy of the License at ##
|
|
11
|
+
## ##
|
|
12
|
+
## http://www.apache.org/licenses/LICENSE-2.0 ##
|
|
13
|
+
## ##
|
|
14
|
+
## Unless required by applicable law or agreed to in writing, software ##
|
|
15
|
+
## distributed under the License is distributed on an "AS IS" BASIS, ##
|
|
16
|
+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ##
|
|
17
|
+
## See the License for the specific language governing permissions and ##
|
|
18
|
+
## limitations under the License. ##
|
|
19
|
+
## ##
|
|
20
|
+
################################################################################
|
|
21
|
+
|
|
22
|
+
"""Internal utilities needed by command-line scripts in _cli_*.py and the
|
|
23
|
+
utilities in cli.py. This is in particular needed to ensure that command-line
|
|
24
|
+
tools can be invoked both on the command line itself, but also from a
|
|
25
|
+
subprocess-free Python API via the
|
|
26
|
+
|
|
27
|
+
Of course, features should as far as possible be available via a dedicated
|
|
28
|
+
pythonic API. For instance, the hfg2ncmat.py module provides a pythonic API for
|
|
29
|
+
the features which can also be accessed via the command-line script
|
|
30
|
+
ncrystal_hfg2ncmat implemented in _cli_hfg2ncmat.py.
|
|
31
|
+
|
|
32
|
+
Notably, command line scripts implemented in their _cli_*.py modules, should:
|
|
33
|
+
|
|
34
|
+
* Use the create_ArgumentParser() below to instantiate their
|
|
35
|
+
argparse.ArgumentParser objects.
|
|
36
|
+
* Provide a create_argparser_for_sphinx( progname ) function which returns an
|
|
37
|
+
ArgumentParser object (before it has been used to parse anything).
|
|
38
|
+
* The scripts can raise normal exceptions, or even SystemExit exceptions. When
|
|
39
|
+
used from the Python API these will be mapped to RuntimeError's.
|
|
40
|
+
* The entry point should be a function def main(argv) which should use the
|
|
41
|
+
"@cli_entry_point" decorator:
|
|
42
|
+
* As all other code in NCrystal Python modules, do not use the normal print
|
|
43
|
+
function, but instead use the print(..) function from the ._common
|
|
44
|
+
module. Likewise, warning's should be emitted via the warn(..) function from
|
|
45
|
+
that module.
|
|
46
|
+
|
|
47
|
+
@cli_entry_point
|
|
48
|
+
def main( argv = None ):
|
|
49
|
+
|
|
50
|
+
TODO: We should have a unit test which checks for all of the above issues!
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
__all__ = []
|
|
55
|
+
|
|
56
|
+
def warn(*a,**kw):
|
|
57
|
+
from ._common import warn
|
|
58
|
+
warn(*a,**kw)
|
|
59
|
+
|
|
60
|
+
def print(*a,**kw):
|
|
61
|
+
from ._common import print
|
|
62
|
+
print(*a,**kw)
|
|
63
|
+
|
|
64
|
+
_argparse_postinitfct = [ None ]
|
|
65
|
+
_argparse_extra_kwargs = [ None ]
|
|
66
|
+
def create_ArgumentParser( *args, **kwargs ):
|
|
67
|
+
"""Always create argparse.ArgumentParser objects from this method, to ensure
|
|
68
|
+
output is redirected while invoking cmdline scripts with cli.run
|
|
69
|
+
"""
|
|
70
|
+
from argparse import ArgumentParser
|
|
71
|
+
for k,v in (_argparse_extra_kwargs[0] or {}).items():
|
|
72
|
+
if k not in kwargs:
|
|
73
|
+
kwargs[k] = v
|
|
74
|
+
parser = ArgumentParser( *args, **kwargs )
|
|
75
|
+
#Ensure consistent argparse --help output from older versions, by changing a
|
|
76
|
+
#section title:
|
|
77
|
+
_fix_argparse_action_group_title(parser,'optional arguments','options')
|
|
78
|
+
f = _argparse_postinitfct[0]
|
|
79
|
+
if f is not None:
|
|
80
|
+
f(parser)
|
|
81
|
+
|
|
82
|
+
thepyversion = _pyversion()
|
|
83
|
+
if thepyversion < (3,13) and hasattr(parser,'_check_value'):
|
|
84
|
+
#Monkey patch object to have same _check_value as in 3.13
|
|
85
|
+
def _check_value( action, value):
|
|
86
|
+
# converted value must be one of the choices (if specified)
|
|
87
|
+
choices = action.choices
|
|
88
|
+
if choices is not None:
|
|
89
|
+
if isinstance(choices, str):
|
|
90
|
+
choices = iter(choices)
|
|
91
|
+
if value not in choices:
|
|
92
|
+
args = {'value': str(value),
|
|
93
|
+
'choices': ', '.join(map(str, action.choices))}
|
|
94
|
+
from gettext import gettext as _
|
|
95
|
+
msg = _('invalid choice: %(value)r'
|
|
96
|
+
' (choose from %(choices)s)')
|
|
97
|
+
from argparse import ArgumentError
|
|
98
|
+
raise ArgumentError(action, msg % args)
|
|
99
|
+
parser._check_value = _check_value
|
|
100
|
+
|
|
101
|
+
if thepyversion < (3,9) and hasattr(parser.formatter_class,'_format_args'):
|
|
102
|
+
#Monkey patch object to fix minor inconsistency in --help
|
|
103
|
+
#formatting:
|
|
104
|
+
orig_format_args = parser.formatter_class._format_args
|
|
105
|
+
def _format_args(self, action, default_metavar):
|
|
106
|
+
get_metavar = self._metavar_formatter(action, default_metavar)
|
|
107
|
+
from argparse import ZERO_OR_MORE
|
|
108
|
+
if action.nargs == ZERO_OR_MORE:
|
|
109
|
+
metavar = get_metavar(1)
|
|
110
|
+
if len(metavar) == 2:
|
|
111
|
+
result = '[%s [%s ...]]' % metavar
|
|
112
|
+
else:
|
|
113
|
+
result = '[%s ...]' % metavar
|
|
114
|
+
return result
|
|
115
|
+
return orig_format_args(self,action,default_metavar)
|
|
116
|
+
parser.formatter_class._format_args = _format_args
|
|
117
|
+
|
|
118
|
+
if thepyversion < (3,13) and hasattr(parser.formatter_class,
|
|
119
|
+
'_format_action_invocation'):
|
|
120
|
+
#Monkey patch object to fix minor inconsistency in --help
|
|
121
|
+
#formatting:
|
|
122
|
+
orig_format_act_invoc = parser.formatter_class._format_action_invocation
|
|
123
|
+
def _format_action_invocation(self, action):
|
|
124
|
+
if action.option_strings and not action.nargs == 0:
|
|
125
|
+
default = self._get_default_metavar_for_optional(action)
|
|
126
|
+
args_string = self._format_args(action, default)
|
|
127
|
+
return ', '.join(action.option_strings) + ' ' + args_string
|
|
128
|
+
else:
|
|
129
|
+
return orig_format_act_invoc(self,action)
|
|
130
|
+
parser.formatter_class._format_action_invocation = _format_action_invocation
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
return parser
|
|
134
|
+
|
|
135
|
+
def _pyversion():
|
|
136
|
+
#returns tuple like (3,13)
|
|
137
|
+
import sys
|
|
138
|
+
return sys.version_info[0:2]
|
|
139
|
+
|
|
140
|
+
class ctxmgr_modify_argparse_creation:
|
|
141
|
+
|
|
142
|
+
def __init__(self, *, exit_on_error, redirect_stderr_to_stdout ):
|
|
143
|
+
self.__exit_on_error = exit_on_error
|
|
144
|
+
self.__redirect_stderr_to_stdout = redirect_stderr_to_stdout
|
|
145
|
+
|
|
146
|
+
def __enter__(self):
|
|
147
|
+
self.__orig_postinitfct = _argparse_postinitfct[0]
|
|
148
|
+
orig_postinitfct = self.__orig_postinitfct
|
|
149
|
+
redirect_stderr_to_stdout = self.__redirect_stderr_to_stdout
|
|
150
|
+
exit_on_error = self.__exit_on_error
|
|
151
|
+
def f(parser):
|
|
152
|
+
if orig_postinitfct:
|
|
153
|
+
orig_postinitfct(parser)
|
|
154
|
+
if not exit_on_error:
|
|
155
|
+
def error(message):
|
|
156
|
+
from argparse import ArgumentError
|
|
157
|
+
raise ArgumentError(None,message)
|
|
158
|
+
parser.error = error
|
|
159
|
+
|
|
160
|
+
if not redirect_stderr_to_stdout:
|
|
161
|
+
return
|
|
162
|
+
#Monkey patch object to redirect stdout/stderr of argparse to
|
|
163
|
+
#NCrystal's print handler.
|
|
164
|
+
if not hasattr(parser,'_print_message'):
|
|
165
|
+
warn("argparse _print_message method (from the unofficial API)"
|
|
166
|
+
" disappeared. Argparse output redirection to NCrystal msg"
|
|
167
|
+
" handler will not work")
|
|
168
|
+
return
|
|
169
|
+
def _print_message( message, file=None):
|
|
170
|
+
if message:
|
|
171
|
+
print(message)
|
|
172
|
+
parser._print_message = _print_message
|
|
173
|
+
|
|
174
|
+
_argparse_postinitfct[0] = f
|
|
175
|
+
self.__orig_extra_kwargs = _argparse_extra_kwargs[0]
|
|
176
|
+
orig_extra_kwargs = self.__orig_extra_kwargs
|
|
177
|
+
new_extra_kwargs = dict( (k,v)
|
|
178
|
+
for k,v in (orig_extra_kwargs or {}).items() )
|
|
179
|
+
#exit_on_error only added in python 3.9:
|
|
180
|
+
if _pyversion() >= (3,9):
|
|
181
|
+
new_extra_kwargs['exit_on_error'] = self.__exit_on_error
|
|
182
|
+
_argparse_extra_kwargs[0] = new_extra_kwargs
|
|
183
|
+
|
|
184
|
+
def __exit__(self,*a,**kw):
|
|
185
|
+
_argparse_postinitfct[0] = self.__orig_postinitfct
|
|
186
|
+
_argparse_extra_kwargs[0] = self.__orig_extra_kwargs
|
|
187
|
+
|
|
188
|
+
_cli_entry_points_called_from_actual_cmdline=[True]
|
|
189
|
+
class _cli_call_from_pyapi_ctx:
|
|
190
|
+
def __enter__(self):
|
|
191
|
+
self.__orig = _cli_entry_points_called_from_actual_cmdline[0]
|
|
192
|
+
_cli_entry_points_called_from_actual_cmdline[0] = False
|
|
193
|
+
def __exit__(self,*a,**kw):
|
|
194
|
+
_cli_entry_points_called_from_actual_cmdline[0] = self.__orig
|
|
195
|
+
|
|
196
|
+
def cli_entry_point(func):
|
|
197
|
+
def mainfct( argv = None ):
|
|
198
|
+
#Called from python-API:
|
|
199
|
+
if not _cli_entry_points_called_from_actual_cmdline[0]:
|
|
200
|
+
assert argv, "Empty argv not ok when calling main in PyAPI mode"
|
|
201
|
+
import os
|
|
202
|
+
progname = argv[0]
|
|
203
|
+
assert progname and isinstance(progname,str)
|
|
204
|
+
assert '/' not in progname and '\\' not in progname
|
|
205
|
+
assert '.' not in progname and '~' not in progname
|
|
206
|
+
arglist = argv[1:]
|
|
207
|
+
return func( progname, arglist )
|
|
208
|
+
#Called from actual command-line, so we translate warnings and
|
|
209
|
+
#exceptions to more suitable printouts:
|
|
210
|
+
from ._common import WarningSpy
|
|
211
|
+
from .exceptions import NCException
|
|
212
|
+
if argv is None:
|
|
213
|
+
import sys
|
|
214
|
+
argv = sys.argv[:]
|
|
215
|
+
assert argv
|
|
216
|
+
import os
|
|
217
|
+
progname = os.path.basename(argv[0])
|
|
218
|
+
arglist = argv[1:]
|
|
219
|
+
def block_warnings(msg_str, cat_str):
|
|
220
|
+
if cat_str == 'NCrystalUserWarning':
|
|
221
|
+
print('WARNING: %s'%msg_str)
|
|
222
|
+
return True
|
|
223
|
+
return False
|
|
224
|
+
with WarningSpy(blockfct=block_warnings):
|
|
225
|
+
try:
|
|
226
|
+
func( progname, arglist )
|
|
227
|
+
except NCException as e:
|
|
228
|
+
n=e.__class__.__name__
|
|
229
|
+
if n.startswith('NC'):
|
|
230
|
+
n = n[2:]
|
|
231
|
+
raise SystemExit('%s ERROR: %s'%(n,e)) from e
|
|
232
|
+
except Exception as e:
|
|
233
|
+
raise SystemExit('ERROR: %s'%(e)) from e
|
|
234
|
+
return mainfct
|
|
235
|
+
|
|
236
|
+
def _resolve_cmd_and_import_climod( cmdname, arguments ):
|
|
237
|
+
resolved_cmd = cli_tool_lookup_impl( cmdname )
|
|
238
|
+
if resolved_cmd is None:
|
|
239
|
+
from .exceptions import NCBadInput
|
|
240
|
+
raise NCBadInput(f'Command line tool name "{cmdname}" not recognised')
|
|
241
|
+
argv = [resolved_cmd['canonical_name']] + [a for a in arguments]
|
|
242
|
+
if resolved_cmd['short_name']=='config':
|
|
243
|
+
clipymodname = '_cliwrap_config'
|
|
244
|
+
else:
|
|
245
|
+
clipymodname = '_cli_%s'%resolved_cmd['short_name']
|
|
246
|
+
import importlib
|
|
247
|
+
climod = importlib.import_module(f'..{clipymodname}', __name__)
|
|
248
|
+
assert hasattr(climod,'main')
|
|
249
|
+
return climod, argv
|
|
250
|
+
|
|
251
|
+
def cli_tool_list_impl( canonical_names = True ):
|
|
252
|
+
# Implementation of cli.cli_tool_list
|
|
253
|
+
import pathlib
|
|
254
|
+
short_names = [ f.name[5:-3] for f in
|
|
255
|
+
pathlib.Path(__file__).parent.glob('_cli_*.py') ]
|
|
256
|
+
short_names.append('config')
|
|
257
|
+
short_names.sort()
|
|
258
|
+
if short_names:
|
|
259
|
+
return short_names
|
|
260
|
+
else:
|
|
261
|
+
return [ _map_shortname_2_canonical_name(sn) for sn in short_names ]
|
|
262
|
+
|
|
263
|
+
def cli_tool_lookup_impl( name ):
|
|
264
|
+
# Implementation of cli.cli_tool_lookup
|
|
265
|
+
|
|
266
|
+
#Note: We basically have to treat only ncrystal-config and nctool as special
|
|
267
|
+
#cases.
|
|
268
|
+
if name == 'ncrystal-config':
|
|
269
|
+
#Special case:
|
|
270
|
+
short_name = 'config'
|
|
271
|
+
elif name.startswith('ncrystal_'):
|
|
272
|
+
short_name = name[9:]
|
|
273
|
+
else:
|
|
274
|
+
short_name = name
|
|
275
|
+
if short_name not in cli_tool_list_impl( canonical_names=False ):
|
|
276
|
+
return None
|
|
277
|
+
return dict( short_name = short_name,
|
|
278
|
+
canonical_name = _map_shortname_2_canonical_name(short_name),
|
|
279
|
+
shellcmd = _map_shortname_2_shellcmd(short_name)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _map_shortname_2_shellcmd( short_name ):
|
|
283
|
+
import pathlib
|
|
284
|
+
is_simplebuild_devel = ( pathlib.Path(__file__).parent
|
|
285
|
+
.joinpath('_is_sblddevel.py').is_file() )
|
|
286
|
+
if not is_simplebuild_devel:
|
|
287
|
+
return { 'config' : 'ncrystal-config',
|
|
288
|
+
'nctool' : 'nctool' }.get( short_name,
|
|
289
|
+
f'ncrystal_{short_name}' )
|
|
290
|
+
else:
|
|
291
|
+
return { 'config' : 'sb_nccmd_config',
|
|
292
|
+
'nctool' : 'sb_nccmd_tool' }.get( short_name,
|
|
293
|
+
f'sb_nccmd_{short_name}' )
|
|
294
|
+
|
|
295
|
+
def _map_shortname_2_canonical_name( short_name ):
|
|
296
|
+
return { 'config' : 'ncrystal-config',
|
|
297
|
+
'nctool' : 'nctool' }.get( short_name,
|
|
298
|
+
f'ncrystal_{short_name}' )
|
|
299
|
+
|
|
300
|
+
def _fix_argparse_action_group_title( parser, oldtitle, newtitle ):
|
|
301
|
+
_ags = getattr(parser,'_action_groups',[])
|
|
302
|
+
if any( getattr(ag,'title','')==newtitle for ag in _ags):
|
|
303
|
+
return
|
|
304
|
+
for ag in _ags:
|
|
305
|
+
if getattr(ag,'title','') == oldtitle:
|
|
306
|
+
setattr(ag,'title',newtitle)
|
|
307
|
+
return
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
################################################################################
|
|
3
|
+
## ##
|
|
4
|
+
## This file is part of NCrystal (see https://mctools.github.io/ncrystal/) ##
|
|
5
|
+
## ##
|
|
6
|
+
## Copyright 2015-2024 NCrystal developers ##
|
|
7
|
+
## ##
|
|
8
|
+
## Licensed under the Apache License, Version 2.0 (the "License"); ##
|
|
9
|
+
## you may not use this file except in compliance with the License. ##
|
|
10
|
+
## You may obtain a copy of the License at ##
|
|
11
|
+
## ##
|
|
12
|
+
## http://www.apache.org/licenses/LICENSE-2.0 ##
|
|
13
|
+
## ##
|
|
14
|
+
## Unless required by applicable law or agreed to in writing, software ##
|
|
15
|
+
## distributed under the License is distributed on an "AS IS" BASIS, ##
|
|
16
|
+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ##
|
|
17
|
+
## See the License for the specific language governing permissions and ##
|
|
18
|
+
## limitations under the License. ##
|
|
19
|
+
## ##
|
|
20
|
+
################################################################################
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from ._cliimpl import cli_entry_point
|
|
24
|
+
|
|
25
|
+
def create_argparser_for_sphinx( progname ):
|
|
26
|
+
raise RuntimeError('Do not call create_argparser_for_sphinx'
|
|
27
|
+
' for ncrystal-config')
|
|
28
|
+
|
|
29
|
+
@cli_entry_point
|
|
30
|
+
def main( progname, arglist ):
|
|
31
|
+
import subprocess
|
|
32
|
+
import shutil
|
|
33
|
+
cmd = shutil.which('ncrystal-config')
|
|
34
|
+
assert cmd, 'ncrystal-config command not found!'
|
|
35
|
+
rv = subprocess.run( [cmd]+arglist[:] )
|
|
36
|
+
raise SystemExit(rv.returncode)
|