11# Copyright Contributors to the Packit project.
22# SPDX-License-Identifier: MIT
3-
3+ import copy
44import datetime
55import logging
66import re
77import types
88from dataclasses import dataclass
9+ from io import IOBase , StringIO
910from pathlib import Path
10- from typing import Generator , List , Optional , Tuple , Type , Union , cast
11+ from typing import (
12+ Any ,
13+ Dict ,
14+ Generator ,
15+ List ,
16+ Optional ,
17+ Tuple ,
18+ Type ,
19+ Union ,
20+ cast ,
21+ )
1122
1223import rpm
1324
4152
4253logger = logging .getLogger (__name__ )
4354
55+ ENCODING_ARGS = {"encoding" : "utf8" , "errors" : "surrogateescape" }
4456
4557class Specfile :
4658 """
@@ -52,7 +64,9 @@ class Specfile:
5264
5365 def __init__ (
5466 self ,
55- path : Union [Path , str ],
67+ path : Optional [Union [Path , str ]] = None ,
68+ file : Optional [IOBase ] = None ,
69+ raw_string : Optional [str ] = None ,
5670 sourcedir : Optional [Union [Path , str ]] = None ,
5771 autosave : bool = False ,
5872 macros : Optional [List [Tuple [str , Optional [str ]]]] = None ,
@@ -63,6 +77,8 @@ def __init__(
6377
6478 Args:
6579 path: Path to the spec file.
80+ file: File object representing the spec file.
81+ raw_string: String containing the spec file content.
6682 sourcedir: Path to sources and patches.
6783 autosave: Whether to automatically save any changes made.
6884 macros: List of extra macro definitions.
@@ -71,12 +87,35 @@ def __init__(
7187 Such sources include sources referenced from shell expansions
7288 in tag values and sources included using the _%include_ directive.
7389 """
90+ if file is not None :
91+ self ._file = file
92+ try :
93+ self ._file .name
94+ except AttributeError :
95+ if sourcedir is None :
96+ raise ValueError (
97+ "'sourcedir' is required when providing a file object without a name"
98+ )
99+ elif path is not None :
100+ self ._file = Path (path ).open ("r+" , ** ENCODING_ARGS )
101+ elif raw_string is not None :
102+ self ._file = StringIO (raw_string )
103+ if sourcedir is None :
104+ raise ValueError (
105+ "'sourcedir' is required when providing a raw string input"
106+ )
107+ else :
108+ raise ValueError (
109+ "Either 'file', 'path', or 'string_input' must be provided"
110+ )
74111 self .autosave = autosave
75- self ._path = Path (path )
76- self ._lines , self ._trailing_newline = self ._read_lines (self ._path )
77- self ._parser = SpecParser (
78- Path (sourcedir or self .path .parent ), macros , force_parse
112+ self ._lines , self ._trailing_newline = self ._read_lines (self ._file )
113+ parser_sourcedir = (
114+ Path (sourcedir )
115+ if sourcedir is not None
116+ else (self .path .parent if self .path else None )
79117 )
118+ self ._parser = SpecParser (parser_sourcedir , macros , force_parse )
80119 self ._parser .parse (str (self ))
81120 self ._dump_debug_info ("After initial parsing" )
82121
@@ -85,7 +124,7 @@ def __eq__(self, other: object) -> bool:
85124 return NotImplemented
86125 return (
87126 self .autosave == other .autosave
88- and self ._path == other ._path
127+ and self .path == other .path
89128 and self ._lines == other ._lines
90129 and self ._parser == other ._parser
91130 )
@@ -111,6 +150,35 @@ def __exit__(
111150 ) -> None :
112151 self .save ()
113152
153+ def __deepcopy__ (self , memodict : Dict [int , Any ]):
154+ """Creates a deep copy of the Specfile, reopens files instantiated from a path,
155+ and returns a new StringIO instance for objects instantiated from StringIO."""
156+ specfile = self .__class__ .__new__ (self .__class__ )
157+ memodict [id (self )] = specfile
158+
159+ for k , v in self .__dict__ .items ():
160+ if k == "_file" :
161+ continue
162+ setattr (specfile , k , copy .deepcopy (v , memodict ))
163+ try :
164+ data = self ._file .getvalue ()
165+ except AttributeError :
166+ try :
167+ path = Path (self ._file .name )
168+ except AttributeError :
169+ raise TypeError (
170+ "Deepcopy is not supported for arbitrary file-like objects"
171+ )
172+ else :
173+ specfile ._file = path .open (
174+ self ._file .mode ,
175+ encoding = self ._file .encoding ,
176+ errors = self ._file .errors ,
177+ )
178+ else :
179+ specfile ._file = type (self ._file )(data )
180+ return specfile
181+
114182 def _dump_debug_info (self , message ) -> None :
115183 logger .debug (
116184 f"DBG: { message } :\n "
@@ -120,18 +188,28 @@ def _dump_debug_info(self, message) -> None:
120188 )
121189
122190 @staticmethod
123- def _read_lines (path : Path ) -> Tuple [List [str ], bool ]:
124- content = path .read_text (encoding = "utf8" , errors = "surrogateescape" )
125- return content .splitlines (), content [- 1 ] == "\n "
191+ def _read_lines (file : IOBase ) -> Tuple [List [str ], bool ]:
192+ file .seek (0 )
193+ raw_content = file .read ()
194+ if isinstance (raw_content , str ):
195+ content = raw_content
196+ else :
197+ content = raw_content .decode (** ENCODING_ARGS )
198+ return content .splitlines (), content .endswith ("\n " )
126199
127200 @property
128- def path (self ) -> Path :
201+ def path (self ) -> Optional [ Path ] :
129202 """Path to the spec file."""
130- return self ._path
203+ try :
204+ return Path (self ._file .name )
205+ except AttributeError :
206+ return None
131207
132208 @path .setter
133209 def path (self , value : Union [Path , str ]) -> None :
134- self ._path = Path (value )
210+ self ._file .close ()
211+ new_path = Path (value )
212+ self ._file = new_path .open ("r+" , ** ENCODING_ARGS )
135213
136214 @property
137215 def sourcedir (self ) -> Path :
@@ -179,11 +257,18 @@ def rpm_spec(self) -> rpm.spec:
179257
180258 def reload (self ) -> None :
181259 """Reloads the spec file content."""
182- self ._lines , self ._trailing_newline = self ._read_lines (self .path )
260+ self ._lines , self ._trailing_newline = self ._read_lines (self ._file )
183261
184262 def save (self ) -> None :
185263 """Saves the spec file content."""
186- self .path .write_text (str (self ), encoding = "utf8" , errors = "surrogateescape" )
264+ self ._file .seek (0 )
265+ self ._file .truncate (0 )
266+ content = str (self )
267+ try :
268+ self ._file .write (content )
269+ except TypeError :
270+ self ._file .write (content .encode (** ENCODING_ARGS ))
271+ self ._file .flush ()
187272
188273 def expand (
189274 self ,
0 commit comments