1# 2# Copyright (C) 2012 - 2018 Satoru SATOH <ssato @ redhat.com> 3# Copyright (C) 2019 Satoru SATOH <satoru.satoh @ gmail.com> 4# License: MIT 5# 6# pylint: disable=unused-argument 7r"""Abstract implementation of backend modules: 8 9Backend module must implement a parser class inherits :class:`Parser` or its 10children classes of this module and override all or some of the methods as 11needed: 12 13 - :meth:`load_from_string`: Load config from string 14 - :meth:`load_from_stream`: Load config from a file or file-like object 15 - :meth:`load_from_path`: Load config from file of given path 16 - :meth:`dump_to_string`: Dump config as a string 17 - :meth:`dump_to_stream`: Dump config to a file or file-like object 18 - :meth:`dump_to_path`: Dump config to a file of given path 19""" 20from __future__ import absolute_import 21 22import functools 23import logging 24import os 25 26import anyconfig.compat 27import anyconfig.globals 28import anyconfig.models.processor 29import anyconfig.utils 30 31 32LOGGER = logging.getLogger(__name__) 33TEXT_FILE = True 34 35 36def ensure_outdir_exists(filepath): 37 """ 38 Make dir to dump 'filepath' if that dir does not exist. 39 40 :param filepath: path of file to dump 41 """ 42 outdir = os.path.dirname(filepath) 43 44 if outdir and not os.path.exists(outdir): 45 LOGGER.debug("Making output dir: %s", outdir) 46 os.makedirs(outdir) 47 48 49def to_method(func): 50 """ 51 Lift :func:`func` to a method; it will be called with the first argument 52 'self' ignored. 53 54 :param func: Any callable object 55 """ 56 @functools.wraps(func) 57 def wrapper(*args, **kwargs): 58 """Wrapper function. 59 """ 60 return func(*args[1:], **kwargs) 61 62 return wrapper 63 64 65def _not_implemented(*args, **kwargs): 66 """ 67 Utility function to raise NotImplementedError. 68 """ 69 raise NotImplementedError() 70 71 72class TextFilesMixin(object): 73 """Mixin class to open configuration files as a plain text. 74 75 Arguments of :func:`open` is different depends on python versions. 76 77 - python 2: https://docs.python.org/2/library/functions.html#open 78 - python 3: https://docs.python.org/3/library/functions.html#open 79 """ 80 _open_flags = ('r', 'w') 81 82 @classmethod 83 def ropen(cls, filepath, **kwargs): 84 """ 85 :param filepath: Path to file to open to read data 86 """ 87 return open(filepath, cls._open_flags[0], **kwargs) 88 89 @classmethod 90 def wopen(cls, filepath, **kwargs): 91 """ 92 :param filepath: Path to file to open to write data to 93 """ 94 return open(filepath, cls._open_flags[1], **kwargs) 95 96 97class BinaryFilesMixin(TextFilesMixin): 98 """Mixin class to open binary (byte string) configuration files. 99 """ 100 _open_flags = ('rb', 'wb') 101 102 103class LoaderMixin(object): 104 """ 105 Mixin class to load data. 106 107 Inherited classes must implement the following methods. 108 109 - :meth:`load_from_string`: Load config from string 110 - :meth:`load_from_stream`: Load config from a file or file-like object 111 - :meth:`load_from_path`: Load config from file of given path 112 113 Member variables: 114 115 - _load_opts: Backend specific options on load 116 - _ordered: True if the parser keep the order of items by default 117 - _allow_primitives: True if the parser.load* may return objects of 118 primitive data types other than mapping types such like JSON parser 119 - _dict_opts: Backend options to customize dict class to make results 120 """ 121 _load_opts = [] 122 _ordered = False 123 _allow_primitives = False 124 _dict_opts = [] 125 126 @classmethod 127 def ordered(cls): 128 """ 129 :return: True if parser can keep the order of keys else False. 130 """ 131 return cls._ordered 132 133 @classmethod 134 def allow_primitives(cls): 135 """ 136 :return: 137 True if the parser.load* may return objects of primitive data types 138 other than mapping types such like JSON parser 139 """ 140 return cls._allow_primitives 141 142 @classmethod 143 def dict_options(cls): 144 """ 145 :return: List of dict factory options 146 """ 147 return cls._dict_opts 148 149 def _container_factory(self, **options): 150 """ 151 The order of prirorities are ac_dict, backend specific dict class 152 option, ac_ordered. 153 154 :param options: Keyword options may contain 'ac_ordered'. 155 :return: Factory (class or function) to make an container. 156 """ 157 ac_dict = options.get("ac_dict", False) 158 _dicts = [x for x in (options.get(o) for o in self.dict_options()) 159 if x] 160 161 if self.dict_options() and ac_dict and callable(ac_dict): 162 return ac_dict # Higher priority than ac_ordered. 163 if _dicts and callable(_dicts[0]): 164 return _dicts[0] 165 if self.ordered() and options.get("ac_ordered", False): 166 return anyconfig.compat.OrderedDict 167 168 return dict 169 170 def _load_options(self, container, **options): 171 """ 172 Select backend specific loading options. 173 """ 174 # Force set dict option if available in backend. For example, 175 # options["object_hook"] will be OrderedDict if 'container' was 176 # OrderedDict in JSON backend. 177 for opt in self.dict_options(): 178 options.setdefault(opt, container) 179 180 return anyconfig.utils.filter_options(self._load_opts, options) 181 182 def load_from_string(self, content, container, **kwargs): 183 """ 184 Load config from given string 'content'. 185 186 :param content: Config content string 187 :param container: callble to make a container object later 188 :param kwargs: optional keyword parameters to be sanitized :: dict 189 190 :return: Dict-like object holding config parameters 191 """ 192 _not_implemented(self, content, container, **kwargs) 193 194 def load_from_path(self, filepath, container, **kwargs): 195 """ 196 Load config from given file path 'filepath`. 197 198 :param filepath: Config file path 199 :param container: callble to make a container object later 200 :param kwargs: optional keyword parameters to be sanitized :: dict 201 202 :return: Dict-like object holding config parameters 203 """ 204 _not_implemented(self, filepath, container, **kwargs) 205 206 def load_from_stream(self, stream, container, **kwargs): 207 """ 208 Load config from given file like object 'stream`. 209 210 :param stream: Config file or file like object 211 :param container: callble to make a container object later 212 :param kwargs: optional keyword parameters to be sanitized :: dict 213 214 :return: Dict-like object holding config parameters 215 """ 216 _not_implemented(self, stream, container, **kwargs) 217 218 def loads(self, content, **options): 219 """ 220 Load config from given string 'content' after some checks. 221 222 :param content: Config file content 223 :param options: 224 options will be passed to backend specific loading functions. 225 please note that options have to be sanitized w/ 226 :func:`anyconfig.utils.filter_options` later to filter out options 227 not in _load_opts. 228 229 :return: dict or dict-like object holding configurations 230 """ 231 container = self._container_factory(**options) 232 if not content or content is None: 233 return container() 234 235 options = self._load_options(container, **options) 236 return self.load_from_string(content, container, **options) 237 238 def load(self, ioi, ac_ignore_missing=False, **options): 239 """ 240 Load config from a file path or a file / file-like object which 'ioi' 241 refering after some checks. 242 243 :param ioi: 244 'anyconfig.globals.IOInfo' namedtuple object provides various info 245 of input object to load data from 246 247 :param ac_ignore_missing: 248 Ignore and just return empty result if given `ioi` object does not 249 exist in actual. 250 :param options: 251 options will be passed to backend specific loading functions. 252 please note that options have to be sanitized w/ 253 :func:`anyconfig.utils.filter_options` later to filter out options 254 not in _load_opts. 255 256 :return: dict or dict-like object holding configurations 257 """ 258 container = self._container_factory(**options) 259 options = self._load_options(container, **options) 260 261 if not ioi: 262 return container() 263 264 if anyconfig.utils.is_stream_ioinfo(ioi): 265 cnf = self.load_from_stream(ioi.src, container, **options) 266 else: 267 if ac_ignore_missing and not os.path.exists(ioi.path): 268 return container() 269 270 cnf = self.load_from_path(ioi.path, container, **options) 271 272 return cnf 273 274 275class DumperMixin(object): 276 """ 277 Mixin class to dump data. 278 279 Inherited classes must implement the following methods. 280 281 - :meth:`dump_to_string`: Dump config as a string 282 - :meth:`dump_to_stream`: Dump config to a file or file-like object 283 - :meth:`dump_to_path`: Dump config to a file of given path 284 285 Member variables: 286 287 - _dump_opts: Backend specific options on dump 288 """ 289 _dump_opts = [] 290 291 def dump_to_string(self, cnf, **kwargs): 292 """ 293 Dump config 'cnf' to a string. 294 295 :param cnf: Configuration data to dump 296 :param kwargs: optional keyword parameters to be sanitized :: dict 297 298 :return: string represents the configuration 299 """ 300 _not_implemented(self, cnf, **kwargs) 301 302 def dump_to_path(self, cnf, filepath, **kwargs): 303 """ 304 Dump config 'cnf' to a file 'filepath'. 305 306 :param cnf: Configuration data to dump 307 :param filepath: Config file path 308 :param kwargs: optional keyword parameters to be sanitized :: dict 309 """ 310 _not_implemented(self, cnf, filepath, **kwargs) 311 312 def dump_to_stream(self, cnf, stream, **kwargs): 313 """ 314 Dump config 'cnf' to a file-like object 'stream'. 315 316 TODO: How to process socket objects same as file objects ? 317 318 :param cnf: Configuration data to dump 319 :param stream: Config file or file like object 320 :param kwargs: optional keyword parameters to be sanitized :: dict 321 """ 322 _not_implemented(self, cnf, stream, **kwargs) 323 324 def dumps(self, cnf, **kwargs): 325 """ 326 Dump config 'cnf' to a string. 327 328 :param cnf: Configuration data to dump 329 :param kwargs: optional keyword parameters to be sanitized :: dict 330 331 :return: string represents the configuration 332 """ 333 kwargs = anyconfig.utils.filter_options(self._dump_opts, kwargs) 334 return self.dump_to_string(cnf, **kwargs) 335 336 def dump(self, cnf, ioi, **kwargs): 337 """ 338 Dump config 'cnf' to output object of which 'ioi' refering. 339 340 :param cnf: Configuration data to dump 341 :param ioi: 342 an 'anyconfig.globals.IOInfo' namedtuple object provides various 343 info of input object to load data from 344 345 :param kwargs: optional keyword parameters to be sanitized :: dict 346 :raises IOError, OSError, AttributeError: When dump failed. 347 """ 348 kwargs = anyconfig.utils.filter_options(self._dump_opts, kwargs) 349 350 if anyconfig.utils.is_stream_ioinfo(ioi): 351 self.dump_to_stream(cnf, ioi.src, **kwargs) 352 else: 353 ensure_outdir_exists(ioi.path) 354 self.dump_to_path(cnf, ioi.path, **kwargs) 355 356 357class Parser(TextFilesMixin, LoaderMixin, DumperMixin, 358 anyconfig.models.processor.Processor): 359 """ 360 Abstract parser to provide basic implementation of some methods, interfaces 361 and members. 362 363 - _type: Parser type indicate which format it supports 364 - _priority: Priority to select it if there are other parsers of same type 365 - _extensions: File extensions of formats it supports 366 - _open_flags: Opening flags to read and write files 367 368 .. seealso:: the doc of :class:`anyconfig.models.processor.Processor` 369 """ 370 pass 371 372 373class FromStringLoaderMixin(LoaderMixin): 374 """ 375 Abstract config parser provides a method to load configuration from string 376 content to help implement parser of which backend lacks of such function. 377 378 Parser classes inherit this class have to override the method 379 :meth:`load_from_string` at least. 380 """ 381 def load_from_stream(self, stream, container, **kwargs): 382 """ 383 Load config from given stream 'stream'. 384 385 :param stream: Config file or file-like object 386 :param container: callble to make a container object later 387 :param kwargs: optional keyword parameters to be sanitized :: dict 388 389 :return: Dict-like object holding config parameters 390 """ 391 return self.load_from_string(stream.read(), container, **kwargs) 392 393 def load_from_path(self, filepath, container, **kwargs): 394 """ 395 Load config from given file path 'filepath'. 396 397 :param filepath: Config file path 398 :param container: callble to make a container object later 399 :param kwargs: optional keyword parameters to be sanitized :: dict 400 401 :return: Dict-like object holding config parameters 402 """ 403 with self.ropen(filepath) as inp: 404 return self.load_from_stream(inp, container, **kwargs) 405 406 407class FromStreamLoaderMixin(LoaderMixin): 408 """ 409 Abstract config parser provides a method to load configuration from string 410 content to help implement parser of which backend lacks of such function. 411 412 Parser classes inherit this class have to override the method 413 :meth:`load_from_stream` at least. 414 """ 415 def load_from_string(self, content, container, **kwargs): 416 """ 417 Load config from given string 'cnf_content'. 418 419 :param content: Config content string 420 :param container: callble to make a container object later 421 :param kwargs: optional keyword parameters to be sanitized :: dict 422 423 :return: Dict-like object holding config parameters 424 """ 425 return self.load_from_stream(anyconfig.compat.StringIO(content), 426 container, **kwargs) 427 428 def load_from_path(self, filepath, container, **kwargs): 429 """ 430 Load config from given file path 'filepath'. 431 432 :param filepath: Config file path 433 :param container: callble to make a container object later 434 :param kwargs: optional keyword parameters to be sanitized :: dict 435 436 :return: Dict-like object holding config parameters 437 """ 438 with self.ropen(filepath) as inp: 439 return self.load_from_stream(inp, container, **kwargs) 440 441 442class ToStringDumperMixin(DumperMixin): 443 """ 444 Abstract config parser provides a method to dump configuration to a file or 445 file-like object (stream) and a file of given path to help implement parser 446 of which backend lacks of such functions. 447 448 Parser classes inherit this class have to override the method 449 :meth:`dump_to_string` at least. 450 """ 451 def dump_to_path(self, cnf, filepath, **kwargs): 452 """ 453 Dump config 'cnf' to a file 'filepath'. 454 455 :param cnf: Configuration data to dump 456 :param filepath: Config file path 457 :param kwargs: optional keyword parameters to be sanitized :: dict 458 """ 459 with self.wopen(filepath) as out: 460 out.write(self.dump_to_string(cnf, **kwargs)) 461 462 def dump_to_stream(self, cnf, stream, **kwargs): 463 """ 464 Dump config 'cnf' to a file-like object 'stream'. 465 466 TODO: How to process socket objects same as file objects ? 467 468 :param cnf: Configuration data to dump 469 :param stream: Config file or file like object 470 :param kwargs: optional keyword parameters to be sanitized :: dict 471 """ 472 stream.write(self.dump_to_string(cnf, **kwargs)) 473 474 475class ToStreamDumperMixin(DumperMixin): 476 """ 477 Abstract config parser provides methods to dump configuration to a string 478 content or a file of given path to help implement parser of which backend 479 lacks of such functions. 480 481 Parser classes inherit this class have to override the method 482 :meth:`dump_to_stream` at least. 483 """ 484 def dump_to_string(self, cnf, **kwargs): 485 """ 486 Dump config 'cnf' to a string. 487 488 :param cnf: Configuration data to dump 489 :param kwargs: optional keyword parameters to be sanitized :: dict 490 491 :return: Dict-like object holding config parameters 492 """ 493 stream = anyconfig.compat.StringIO() 494 self.dump_to_stream(cnf, stream, **kwargs) 495 return stream.getvalue() 496 497 def dump_to_path(self, cnf, filepath, **kwargs): 498 """ 499 Dump config 'cnf' to a file 'filepath`. 500 501 :param cnf: Configuration data to dump 502 :param filepath: Config file path 503 :param kwargs: optional keyword parameters to be sanitized :: dict 504 """ 505 with self.wopen(filepath) as out: 506 self.dump_to_stream(cnf, out, **kwargs) 507 508 509class StringParser(Parser, FromStringLoaderMixin, ToStringDumperMixin): 510 """ 511 Abstract parser based on :meth:`load_from_string` and 512 :meth:`dump_to_string`. 513 514 Parser classes inherit this class must define these methods. 515 """ 516 pass 517 518 519class StreamParser(Parser, FromStreamLoaderMixin, ToStreamDumperMixin): 520 """ 521 Abstract parser based on :meth:`load_from_stream` and 522 :meth:`dump_to_stream`. 523 524 Parser classes inherit this class must define these methods. 525 """ 526 pass 527 528 529def load_with_fn(load_fn, content_or_strm, container, allow_primitives=False, 530 **options): 531 """ 532 Load data from given string or stream 'content_or_strm'. 533 534 :param load_fn: Callable to load data 535 :param content_or_strm: data content or stream provides it 536 :param container: callble to make a container object 537 :param allow_primitives: 538 True if the parser.load* may return objects of primitive data types 539 other than mapping types such like JSON parser 540 :param options: keyword options passed to 'load_fn' 541 542 :return: container object holding data 543 """ 544 ret = load_fn(content_or_strm, **options) 545 if anyconfig.utils.is_dict_like(ret): 546 return container() if (ret is None or not ret) else container(ret) 547 548 return ret if allow_primitives else container(ret) 549 550 551def dump_with_fn(dump_fn, data, stream, **options): 552 """ 553 Dump 'data' to a string if 'stream' is None, or dump 'data' to a file or 554 file-like object 'stream'. 555 556 :param dump_fn: Callable to dump data 557 :param data: Data to dump 558 :param stream: File or file like object or None 559 :param options: optional keyword parameters 560 561 :return: String represents data if stream is None or None 562 """ 563 if stream is None: 564 return dump_fn(data, **options) 565 566 return dump_fn(data, stream, **options) 567 568 569class StringStreamFnParser(Parser, FromStreamLoaderMixin, ToStreamDumperMixin): 570 """ 571 Abstract parser utilizes load and dump functions each backend module 572 provides such like json.load{,s} and json.dump{,s} in JSON backend. 573 574 Parser classes inherit this class must define the followings. 575 576 - _load_from_string_fn: Callable to load data from string 577 - _load_from_stream_fn: Callable to load data from stream (file object) 578 - _dump_to_string_fn: Callable to dump data to string 579 - _dump_to_stream_fn: Callable to dump data to stream (file object) 580 581 .. note:: 582 Callables have to be wrapped with :func:`to_method` to make 'self' 583 passed to the methods created from them ignoring it. 584 585 :seealso: :class:`anyconfig.backend.json.Parser` 586 """ 587 _load_from_string_fn = None 588 _load_from_stream_fn = None 589 _dump_to_string_fn = None 590 _dump_to_stream_fn = None 591 592 def load_from_string(self, content, container, **options): 593 """ 594 Load configuration data from given string 'content'. 595 596 :param content: Configuration string 597 :param container: callble to make a container object 598 :param options: keyword options passed to '_load_from_string_fn' 599 600 :return: container object holding the configuration data 601 """ 602 return load_with_fn(self._load_from_string_fn, content, container, 603 allow_primitives=self.allow_primitives(), 604 **options) 605 606 def load_from_stream(self, stream, container, **options): 607 """ 608 Load data from given stream 'stream'. 609 610 :param stream: Stream provides configuration data 611 :param container: callble to make a container object 612 :param options: keyword options passed to '_load_from_stream_fn' 613 614 :return: container object holding the configuration data 615 """ 616 return load_with_fn(self._load_from_stream_fn, stream, container, 617 allow_primitives=self.allow_primitives(), 618 **options) 619 620 def dump_to_string(self, cnf, **kwargs): 621 """ 622 Dump config 'cnf' to a string. 623 624 :param cnf: Configuration data to dump 625 :param kwargs: optional keyword parameters to be sanitized :: dict 626 627 :return: string represents the configuration 628 """ 629 return dump_with_fn(self._dump_to_string_fn, cnf, None, **kwargs) 630 631 def dump_to_stream(self, cnf, stream, **kwargs): 632 """ 633 Dump config 'cnf' to a file-like object 'stream'. 634 635 TODO: How to process socket objects same as file objects ? 636 637 :param cnf: Configuration data to dump 638 :param stream: Config file or file like object 639 :param kwargs: optional keyword parameters to be sanitized :: dict 640 """ 641 dump_with_fn(self._dump_to_stream_fn, cnf, stream, **kwargs) 642 643# vim:sw=4:ts=4:et: 644