files.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. """Contains writer for writing nbconvert output to filesystem."""
  2. # Copyright (c) IPython Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import errno
  5. import glob
  6. import os
  7. from pathlib import Path
  8. from traitlets import Unicode, observe
  9. from nbconvert.utils.io import link_or_copy
  10. from .base import WriterBase
  11. class FilesWriter(WriterBase):
  12. """Consumes nbconvert output and produces files."""
  13. build_directory = Unicode(
  14. "",
  15. help="""Directory to write output(s) to. Defaults
  16. to output to the directory of each notebook. To recover
  17. previous default behaviour (outputting to the current
  18. working directory) use . as the flag value.""",
  19. ).tag(config=True)
  20. relpath = Unicode(
  21. help="""When copying files that the notebook depends on, copy them in
  22. relation to this path, such that the destination filename will be
  23. os.path.relpath(filename, relpath). If FilesWriter is operating on a
  24. notebook that already exists elsewhere on disk, then the default will be
  25. the directory containing that notebook."""
  26. ).tag(config=True)
  27. # Make sure that the output directory exists.
  28. @observe("build_directory")
  29. def _build_directory_changed(self, change):
  30. new = change["new"]
  31. if new:
  32. self._makedir(new)
  33. def __init__(self, **kw):
  34. """Initialize the writer."""
  35. super().__init__(**kw)
  36. self._build_directory_changed({"new": self.build_directory})
  37. def _makedir(self, path, mode=0o755):
  38. """ensure that a directory exists
  39. If it doesn't exist, try to create it and protect against a race condition
  40. if another process is doing the same.
  41. The default permissions are 755, which differ from os.makedirs default of 777.
  42. """
  43. if not os.path.exists(path):
  44. self.log.info("Making directory %s", path)
  45. try:
  46. os.makedirs(path, mode=mode)
  47. except OSError as e:
  48. if e.errno != errno.EEXIST:
  49. raise
  50. elif not os.path.isdir(path):
  51. raise OSError("%r exists but is not a directory" % path)
  52. def _write_items(self, items, build_dir):
  53. """Write a dict containing filename->binary data"""
  54. for filename, data in items:
  55. # Determine where to write the file to
  56. dest = os.path.join(build_dir, filename)
  57. path = os.path.dirname(dest)
  58. self._makedir(path)
  59. # Write file
  60. self.log.debug("Writing %i bytes to %s", len(data), dest)
  61. with open(dest, "wb") as f:
  62. f.write(data)
  63. def write(self, output, resources, notebook_name=None, **kw):
  64. """
  65. Consume and write Jinja output to the file system. Output directory
  66. is set via the 'build_directory' variable of this instance (a
  67. configurable).
  68. See base for more...
  69. """
  70. # Verify that a notebook name is provided.
  71. if notebook_name is None:
  72. msg = "notebook_name"
  73. raise TypeError(msg)
  74. # Pull the extension and subdir from the resources dict.
  75. output_extension = resources.get("output_extension", None)
  76. # Get the relative path for copying files
  77. resource_path = resources.get("metadata", {}).get("path", "")
  78. relpath = self.relpath or resource_path
  79. build_directory = self.build_directory or resource_path
  80. # Write the extracted outputs to the destination directory.
  81. # NOTE: WE WRITE EVERYTHING AS-IF IT'S BINARY. THE EXTRACT FIG
  82. # PREPROCESSOR SHOULD HANDLE UNIX/WINDOWS LINE ENDINGS...
  83. items = resources.get("outputs", {}).items()
  84. if items:
  85. self.log.info(
  86. "Support files will be in %s",
  87. os.path.join(resources.get("output_files_dir", ""), ""),
  88. )
  89. self._write_items(items, build_directory)
  90. # Write the extracted attachments
  91. # if ExtractAttachmentsOutput specified a separate directory
  92. attachments = resources.get("attachments", {}).items()
  93. if attachments:
  94. self.log.info(
  95. "Attachments will be in %s",
  96. os.path.join(resources.get("attachment_files_dir", ""), ""),
  97. )
  98. self._write_items(attachments, build_directory)
  99. # Copy referenced files to output directory
  100. if build_directory:
  101. for filename in self.files:
  102. # Copy files that match search pattern
  103. for matching_filename in glob.glob(filename):
  104. # compute the relative path for the filename
  105. if relpath != "":
  106. dest_filename = os.path.relpath(matching_filename, relpath)
  107. else:
  108. dest_filename = matching_filename
  109. # Make sure folder exists.
  110. dest = os.path.join(build_directory, dest_filename)
  111. path = os.path.dirname(dest)
  112. self._makedir(path)
  113. # Copy if destination is different.
  114. if os.path.normpath(dest) != os.path.normpath(matching_filename):
  115. self.log.info("Copying %s -> %s", matching_filename, dest)
  116. link_or_copy(matching_filename, dest)
  117. # Determine where to write conversion results.
  118. dest = notebook_name + output_extension if output_extension is not None else notebook_name
  119. dest_path = Path(build_directory) / dest
  120. # Write conversion results.
  121. self.log.info("Writing %i bytes to %s", len(output), dest_path)
  122. if isinstance(output, str):
  123. with open(dest_path, "w", encoding="utf-8") as f:
  124. f.write(output)
  125. else:
  126. with open(dest_path, "wb") as f:
  127. f.write(output)
  128. return dest_path