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.
Files changed (53) hide show
  1. NCrystal/__init__.py +85 -0
  2. NCrystal/__main__.py +98 -0
  3. NCrystal/_chooks.py +854 -0
  4. NCrystal/_cli_cif2ncmat.py +269 -0
  5. NCrystal/_cli_endf2ncmat.py +503 -0
  6. NCrystal/_cli_hfg2ncmat.py +144 -0
  7. NCrystal/_cli_mcstasunion.py +74 -0
  8. NCrystal/_cli_ncmat2cpp.py +31 -0
  9. NCrystal/_cli_ncmat2hkl.py +180 -0
  10. NCrystal/_cli_nctool.py +1018 -0
  11. NCrystal/_cli_vdos2ncmat.py +463 -0
  12. NCrystal/_cli_verifyatompos.py +257 -0
  13. NCrystal/_cliimpl.py +307 -0
  14. NCrystal/_cliwrap_config.py +36 -0
  15. NCrystal/_common.py +499 -0
  16. NCrystal/_coreimpl.py +114 -0
  17. NCrystal/_hfgdata.py +546 -0
  18. NCrystal/_hklobjects.py +136 -0
  19. NCrystal/_is_std.py +0 -0
  20. NCrystal/_locatelib.py +210 -0
  21. NCrystal/_miscimpl.py +354 -0
  22. NCrystal/_mmc.py +757 -0
  23. NCrystal/_msg.py +60 -0
  24. NCrystal/_ncmat2cpp_impl.py +445 -0
  25. NCrystal/_ncmatimpl.py +2131 -0
  26. NCrystal/_numpy.py +76 -0
  27. NCrystal/_testimpl.py +579 -0
  28. NCrystal/api.py +56 -0
  29. NCrystal/atomdata.py +177 -0
  30. NCrystal/cfgstr.py +77 -0
  31. NCrystal/cifutils.py +1795 -0
  32. NCrystal/cli.py +96 -0
  33. NCrystal/constants.py +134 -0
  34. NCrystal/core.py +1910 -0
  35. NCrystal/datasrc.py +226 -0
  36. NCrystal/exceptions.py +66 -0
  37. NCrystal/hfg2ncmat.py +270 -0
  38. NCrystal/mcstasutils.py +438 -0
  39. NCrystal/misc.py +317 -0
  40. NCrystal/mmc.py +35 -0
  41. NCrystal/ncmat.py +778 -0
  42. NCrystal/ncmat2cpp.py +80 -0
  43. NCrystal/obsolete.py +67 -0
  44. NCrystal/plot.py +484 -0
  45. NCrystal/plugins.py +49 -0
  46. NCrystal/test.py +76 -0
  47. NCrystal/vdos.py +1034 -0
  48. ncrystal_python-3.9.81.dist-info/LICENSE +206 -0
  49. ncrystal_python-3.9.81.dist-info/METADATA +515 -0
  50. ncrystal_python-3.9.81.dist-info/RECORD +53 -0
  51. ncrystal_python-3.9.81.dist-info/WHEEL +5 -0
  52. ncrystal_python-3.9.81.dist-info/entry_points.txt +10 -0
  53. 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)