|
2 | 2 | import logging
|
3 | 3 | import inspect
|
4 | 4 | import re
|
| 5 | +import collections |
| 6 | +import itertools |
5 | 7 | from .connection import conn
|
6 |
| -from .diagram import Diagram |
| 8 | +from .diagram import Diagram, _get_tier |
7 | 9 | from .settings import config
|
8 | 10 | from .errors import DataJointError, AccessError
|
9 | 11 | from .jobs import JobTable
|
10 | 12 | from .external import ExternalMapping
|
11 | 13 | from .heading import Heading
|
12 | 14 | from .utils import user_choice, to_camel_case
|
13 | 15 | from .user_tables import Part, Computed, Imported, Manual, Lookup
|
14 |
| -from .table import lookup_class_name, Log |
| 16 | +from .table import lookup_class_name, Log, FreeTable |
15 | 17 | import types
|
16 | 18 |
|
17 | 19 | logger = logging.getLogger(__name__.split(".")[0])
|
@@ -399,6 +401,78 @@ def jobs(self):
|
399 | 401 | self._jobs = JobTable(self.connection, self.database)
|
400 | 402 | return self._jobs
|
401 | 403 |
|
| 404 | + @property |
| 405 | + def code(self): |
| 406 | + self._assert_exists() |
| 407 | + return self.save() |
| 408 | + |
| 409 | + def save(self, python_filename=None): |
| 410 | + """ |
| 411 | + Generate the code for a module that recreates the schema. |
| 412 | + This method is in preparation for a future release and is not officially supported. |
| 413 | +
|
| 414 | + :return: a string containing the body of a complete Python module defining this schema. |
| 415 | + """ |
| 416 | + self._assert_exists() |
| 417 | + module_count = itertools.count() |
| 418 | + # add virtual modules for referenced modules with names vmod0, vmod1, ... |
| 419 | + module_lookup = collections.defaultdict( |
| 420 | + lambda: "vmod" + str(next(module_count)) |
| 421 | + ) |
| 422 | + db = self.database |
| 423 | + |
| 424 | + def make_class_definition(table): |
| 425 | + tier = _get_tier(table).__name__ |
| 426 | + class_name = table.split(".")[1].strip("`") |
| 427 | + indent = "" |
| 428 | + if tier == "Part": |
| 429 | + class_name = class_name.split("__")[-1] |
| 430 | + indent += " " |
| 431 | + class_name = to_camel_case(class_name) |
| 432 | + |
| 433 | + def replace(s): |
| 434 | + d, tabs = s.group(1), s.group(2) |
| 435 | + return ("" if d == db else (module_lookup[d] + ".")) + ".".join( |
| 436 | + to_camel_case(tab) for tab in tabs.lstrip("__").split("__") |
| 437 | + ) |
| 438 | + |
| 439 | + return ("" if tier == "Part" else "\n@schema\n") + ( |
| 440 | + "{indent}class {class_name}(dj.{tier}):\n" |
| 441 | + '{indent} definition = """\n' |
| 442 | + '{indent} {defi}"""' |
| 443 | + ).format( |
| 444 | + class_name=class_name, |
| 445 | + indent=indent, |
| 446 | + tier=tier, |
| 447 | + defi=re.sub( |
| 448 | + r"`([^`]+)`.`([^`]+)`", |
| 449 | + replace, |
| 450 | + FreeTable(self.connection, table).describe(), |
| 451 | + ).replace("\n", "\n " + indent), |
| 452 | + ) |
| 453 | + |
| 454 | + diagram = Diagram(self) |
| 455 | + body = "\n\n".join( |
| 456 | + make_class_definition(table) for table in diagram.topological_sort() |
| 457 | + ) |
| 458 | + python_code = "\n\n".join( |
| 459 | + ( |
| 460 | + '"""This module was auto-generated by datajoint from an existing schema"""', |
| 461 | + "import datajoint as dj\n\nschema = dj.Schema('{db}')".format(db=db), |
| 462 | + "\n".join( |
| 463 | + "{module} = dj.VirtualModule('{module}', '{schema_name}')".format( |
| 464 | + module=v, schema_name=k |
| 465 | + ) |
| 466 | + for k, v in module_lookup.items() |
| 467 | + ), |
| 468 | + body, |
| 469 | + ) |
| 470 | + ) |
| 471 | + if python_filename is None: |
| 472 | + return python_code |
| 473 | + with open(python_filename, "wt") as f: |
| 474 | + f.write(python_code) |
| 475 | + |
402 | 476 | def list_tables(self):
|
403 | 477 | """
|
404 | 478 | Return a list of all tables in the schema except tables with ~ in first character such
|
|
0 commit comments