How can I keep comments of a yaml file when modify its values in nested dictionaries using ruamel.yaml - comments

I'm using solution in the related answer for How to auto-dump modified values in nested dictionaries using ruamel.yaml, which uses the default (round-trip) loader/dumper.
I believe, it is a hard problem since additinal dict should keep comments as well.
=> Would it be possible to prevent comments to be removed if we use modify values in nested dictionaries using ruamel.yaml approach?
Example:
config.yaml:
c: # my comment
b:
f: 5
a:
z: 4
b: 4 # my comment
code (same code from How to auto-dump modified values in nested dictionaries using ruamel.yaml
), which changed to use the default (round-trip) loader/dumper:
#!/usr/bin/env python3
from pathlib import Path
from ruamel.yaml import YAML, representer
class SubConfig(dict):
def __init__(self, parent):
self.parent = parent
def updated(self):
self.parent.updated()
def __setitem__(self, key, value):
if isinstance(value, dict):
v = SubConfig(self)
v.update(value)
value = v
super().__setitem__(key, value)
self.updated()
def __getitem__(self, key):
try:
res = super().__getitem__(key)
except KeyError:
super().__setitem__(key, SubConfig(self))
self.updated()
return super().__getitem__(key)
return res
def __delitem__(self, key):
res = super().__delitem__(key)
self.updated()
def update(self, *args, **kw):
for arg in args:
for k, v in arg.items():
self[k] = v
for k, v in kw.items():
self[k] = v
self.updated()
return
_SR = representer.RoundTripRepresenter
_SR.add_representer(SubConfig, _SR.represent_dict)
class Config(dict):
def __init__(self, filename, auto_dump=True):
self.filename = filename if hasattr(filename, "open") else Path(filename)
self.auto_dump = auto_dump
self.changed = False
self.yaml = YAML()
self.yaml.indent(mapping=4, sequence=4, offset=2)
self.yaml.default_flow_style = False
if self.filename.exists():
with open(filename) as f:
self.update(self.yaml.load(f) or {})
def updated(self):
if self.auto_dump:
self.dump(force=True)
else:
self.changed = True
def dump(self, force=False):
if not self.changed and not force:
return
with open(self.filename, "w") as f:
self.yaml.dump(dict(self), f)
self.changed = False
def __setitem__(self, key, value):
if isinstance(value, dict):
v = SubConfig(self)
v.update(value)
value = v
super().__setitem__(key, value)
self.updated()
def __getitem__(self, key):
try:
res = super().__getitem__(key)
except KeyError:
super().__setitem__(key, SubConfig(self))
self.updated()
return super().__getitem__(key)
def __delitem__(self, key):
res = super().__delitem__(key)
self.updated()
def update(self, *args, **kw):
for arg in args:
for k, v in arg.items():
self[k] = v
for k, v in kw.items():
self[k] = v
self.updated()
cfg = Config(Path("config.yaml"))
=> config.yaml file is updated as follows, where its comments are removed:
c:
b:
f: 5
a:
z: 4
b: 4

Yes, it is possible to prevent comments from being lost. The object returned from self.yaml.load() in your Config.__init__() method is not a dict, but a subclass thereof (ruamel.yaml.comments.CommentedMap) that includes all of the comment information (in its .ca attribute. And that CommentedMap will have values that are in itself again CommentedMap instances (at least with your input.
So what you need to do is change your classes:
class Config(ruamel.yaml.comments.CommentedMap):
and do the same for SubConfig. Then during the update routine you should try to copy the .ca attribute (it will be created empty on the CommentedMap, but not be available on {})
Make sure you all add a the representer for Config, and don't cast to dict in your Config.dump() method.
If you also copy the .fa attribute of the loaded data (also on Subconfig), you'll preserve the flow/block style of the original, and you can do a away with the self.yaml.default_flow_style = False.
The above is the theory, in practise there are a few more issues.
Your config.yaml changes, although you do not explicitly dump. That is
because your auto_dump is True by default. But that also means it
dumps for every change, i.e. your config.yaml gets dumped 10 (ten) times while the
Config/SubConfig data structure gets build.
I changed this to dump only once if auto_dump is True, but even that I would not
recommend, instead only dump if changed after loading.
A dict doesn't need initializing, but a CommentedMap does.
If you don't you get an attribute error at some point. So you'll have to
call super().__init__(self) in each the __init__ of each class.
from pathlib import Path
import ruamel.yaml
_SR = ruamel.yaml.representer.RoundTripRepresenter
class SubConfig(ruamel.yaml.comments.CommentedMap):
def __init__(self, parent):
self.parent = parent
super().__init__(self)
def updated(self):
self.parent.updated()
def __setitem__(self, key, value):
if isinstance(value, dict):
v = SubConfig(self)
v.update(value)
value = v
super().__setitem__(key, value)
self.updated()
def __getitem__(self, key):
try:
res = super().__getitem__(key)
except KeyError:
super().__setitem__(key, SubConfig(self))
self.updated()
return super().__getitem__(key)
return res
def __delitem__(self, key):
res = super().__delitem__(key)
self.updated()
def update(self, *args, **kw):
for arg in args:
for k, v in arg.items():
self[k] = v
for attr in [ruamel.yaml.comments.Comment.attrib, ruamel.yaml.comments.Format.attrib]:
if hasattr(arg, attr):
setattr(self, attr, getattr(arg, attr))
for k, v in kw.items():
self[k] = v
self.updated()
return
_SR.add_representer(SubConfig, _SR.represent_dict)
class Config(ruamel.yaml.comments.CommentedMap):
def __init__(self, filename, auto_dump=True):
super().__init__(self)
self.filename = filename if hasattr(filename, "open") else Path(filename)
self.auto_dump = False # postpone setting during loading of config
self.changed = False
self.yaml = ruamel.yaml.YAML()
self.yaml.indent(mapping=4, sequence=4, offset=2)
# self.yaml.default_flow_style = False
if self.filename.exists():
with open(filename) as f:
self.update(self.yaml.load(f) or {})
self.auto_dump = auto_dump
if auto_dump:
self.dump()
def updated(self):
if self.auto_dump:
self.dump(force=True)
else:
self.changed = True
def dump(self, force=False):
if not self.changed and not force:
return
# use the capability of dump to take a Path. It will open the file 'wb' as
# is appropriate for a YAML file, which is UTF-8
self.yaml.dump(self, self.filename)
self.changed = False
def __setitem__(self, key, value):
if isinstance(value, dict):
v = SubConfig(self)
v.update(value)
value = v
super().__setitem__(key, value)
self.updated()
def __getitem__(self, key):
try:
res = super().__getitem__(key)
except KeyError:
super().__setitem__(key, SubConfig(self))
self.updated()
return super().__getitem__(key)
def __delitem__(self, key):
res = super().__delitem__(key)
self.updated()
def update(self, *args, **kw):
for arg in args:
for k, v in arg.items():
self[k] = v
for attr in [ruamel.yaml.comments.Comment.attrib, ruamel.yaml.comments.Format.attrib]:
if hasattr(arg, attr):
setattr(self, attr, getattr(arg, attr))
for k, v in kw.items():
self[k] = v
self.updated()
_SR.add_representer(Config, _SR.represent_dict)
fn = Path('config.yaml')
fn.write_text("""
c: # my comment
b:
f: 5
x: {g: 6}
a:
z: 4
b: 4 # my comment
""")
cfg = Config(fn)
print(Path(fn).read_text())
which gives:
c: # my comment
b:
f: 5
x: {g: 6}
a:
z: 4
b: 4 # my comment
Since the input changes, I write the config file to test out on every run.
I also added a flow style dict, to make clear that original formatting is performed.

Related

Filtering a QTableview

please have a look at the following code:
import timeit
from builtins import super
import pandas as pd
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, QSortFilterProxyModel
from PyQt5.QtGui import QBrush
from PyQt5.uic import loadUi
class PandasTableModel(QtGui.QStandardItemModel):
def __init__(self, data, parent=None):
QtGui.QStandardItemModel.__init__(self, parent)
self._data = data
for col in data.columns:
data_col = [QtGui.QStandardItem("{}".format(x)) for x in data[col].values]
self.appendColumn(data_col)
return
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def headerData(self, x, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[x]
if orientation == Qt.Vertical and role == Qt.DisplayRole:
return self._data.index[x]
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled
return super().flags(index) | Qt.ItemIsEditable # add editable flag.
def setData(self, index, value, role):
if role == Qt.EditRole:
# Set the value into the frame.
self._data.iloc[index.row(), index.column()] = value
return True
return False
class TableViewer(QtWidgets.QMainWindow):
def __init__(self):
super(TableViewer, self).__init__()
self.ui = loadUi("QTableViewForm.ui", self)
self.ui.cmdRun1.clicked.connect(self.RunFunction1)
self.ui.cmdRun2.clicked.connect(self.RunFunction2)
self.ui.inputFilter.textChanged.connect(self.SetFilteredView)
self.showdata()
def showdata(self):
start = timeit.default_timer()
print("Start LoadData")
data = pd.read_pickle("productdata.pkl")
self.model = PandasTableModel(data)
self.ui.tableData.setModel(self.model)
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setFilterKeyColumn(-1) # Search all columns.
self.proxy_model.setSourceModel(self.model)
self.proxy_model.sort(0, Qt.AscendingOrder)
self.proxy_model.setFilterCaseSensitivity(False)
self.ui.tableData.setModel(self.proxy_model)
print("Stop LoadData")
end = timeit.default_timer()
print("Process Time: ", (end - start))
def RunFunction1(self):
start = timeit.default_timer()
print("Start RunFunction1")
#Achtergrondkleur van de cel of rij is aanpasbaar:
item=self.model.item(2,2)
item.setBackground(QBrush(Qt.red))
print("Stop RunFunction1")
end = timeit.default_timer()
print("Process Time: ", (end - start))
def RunFunction2(self):
start = timeit.default_timer()
print("Start RunFunction1")
for i in range(10):
item = self.model.item(2, i)
item.setBackground(QBrush(Qt.green))
print("Stop RunFunction1")
end = timeit.default_timer()
print("Process Time: ", (end - start))
def SetFilteredView(self):
# print("Start set_filter")
filter_text = self.ui.inputFilter.text()
self.proxy_model.setFilterFixedString(filter_text)
filter_result = self.proxy_model.rowCount()
self.ui.lblResult.setText("(" + str(filter_result) + " records)")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
win = TableViewer()
win.show()
sys.exit(app.exec_())
I have a window with a filter input field. The filtering works great and really fast. Now i would like to be able to enter more than 1 entry to narrow the search. (Basicly an "AND" function in the filterstring).
Any suggestions how to do this ?
Cheers Johnson

How should I convert it to the real number?

I tried both str and repr. but both of them doesn't work and still return <main.UnorderedList object at 0x10cefccc0> these things.
class Stack:
def __init__(self):
self.items = UnorderedList()
def isEmpty(self):
return self.items == []
def push(self, item):
self.items.append(item)
def __repr__(self):
return str(self.items)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items) - 1]
def size(self):
length = self.items.size()
count = 0
for i in range(0, length):
count += 1
return count
print(stack_list.__repr__())
result is:
[<main.Node object at 0x10db27c18>, <main.Node object at 0x10db27d30>]
If you are interested in getting the output you wrote after "result is" then (your question is a bit unclear and) you can use this code:
def __repr__(self):
return '[ %s ]' % ', '.join(repr(item) for item in self.items)
The __repr__() function is supposed to return a representation of the object, though, which should recreate the object again when interpreted. So consider this instead:
class Stack:
def __init__(self, items):
self.items = UnorderedList(items)
def __repr__(self):
return 'Stack([ %s ])' % ', '.join(repr(item) for item in self.items)
This will produce repr output like Stack([ "foo", "bar" ]).
I find it peculiar, though, that you program a stack which is based on an unordered list. This is confusing and I would not recommend to do that in the first place. Normally, stacks have a fixed order of their items.

how to apply a function to each values of `params`

I am trying to pre-process the params with to_ar2en_i function in ApplicationController before any action processes the params, I have the following in my application_controller.rb:
# translates every params' entity from arabic to english
before_action :param_convert_ar2en_i
private
def param_convert_ar2en_i h = nil, path = []
h ||= params
h.each_pair do |k, v|
if v.respond_to?(:key?)
param_convert_ar2en_i v, [path, k].flatten
else
# something like:
params[[path, k].flatten].to_ar2en_i
end
end
end
The problem is I don't know how to apply to_ar2en_i to a nested params with the path of [[path, k].flatten].
Can anybody kindly help me on this?
Silly me!! Instead of trying to access params in params[[path, k].flatten].to_ar2en_i all I needed to do is just to access h[k].to_ar2en_i and since the h is passed by reference in Ruby, it will do the job.
def param_convert_ar2en_i h = nil, l = [], depth: 0
h ||= params
h.each_pair do |k, v|
if v.respond_to? :key?
param_convert_ar2en_i v, [l, k].flatten, depth: depth + 1
else
if h[k].respond_to? :each
h[k].each { |i| i.to_ar2en_i if i.respond_to? :to_ar2en_i }
else
h[k].to_ar2en_i if h[k].respond_to? :to_ar2en_i
end
end
end
end

How to convert a ruby lambda to a reusable library method

I have a method in a chef recipe like so
overwrite_properties = lambda do |tmpl_path, params|
attrs = {}
File.read(tmpl_path).split("\n").map do |line|
line = line.sub(/#.*$/, '').strip
j, v = line.split(/\s*=\s*/, 2)
attrs[j] = v if j
end
params.each {|j,v| v.nil? ? attrs.delete(j) : attrs[j] = v }
attrs.map {|j,v| "#{j}=#{v}\n" }.sort.join
end
which is called like so
overwrite_properties.call("#{server_home}/config.orig/#{fname}", params)
My question is how can I convert this to a reusable function to be called from another module?
eg.
module HelperMod
def self.overwrite_properties(&block)
//etc
end
end
Thanks
This is simple - a lambda is an anonymous function (function with no name). You can convert this to a function within a module like so:
module HelperMod
def self.overwrite_properties(tmpl_path, params)
attrs = {}
File.read(tmpl_path).split("\n").map do |line|
line = line.sub(/#.*$/, '').strip
j, v = line.split(/\s*=\s*/, 2)
attrs[j] = v if j
end
params.each {|j,v| v.nil? ? attrs.delete(j) : attrs[j] = v }
attrs.map {|j,v| "#{j}=#{v}\n" }.sort.join
end
end
HelperMod.overwrite_properties("#{server_home}/config.orig/#{fname}", params)

Check if two linked lists are equal in Ruby?

I have the following implementation of a linked list in Ruby:
class Node
attr_accessor :data, :next
def initialize(data = nil)
#data = data
#next = nil
end
end
class LinkedList
def initialize(items)
#head = Node.new(items.shift)
items.inject(#head) { |last, data| #tail = last.next = Node.new(data) }
end
def iterate
return nil if #head.nil?
entry = #head
until entry.nil?
yield entry
entry = entry.next
end
end
def equal?(other_list)
#How do I check if all the data for all the elements in one list are the same in the other one?
end
end
I have tried using the .iterate like this:
def equals?(other_list)
other_list.iterate do |ol|
self.iterate do |sl|
if ol.data != sl.data
return false
end
end
end
return true
end
But this is doing a nested approach. I fail to see how to do it.
You can't do it easily with the methods you have defined currently, as there is no way to access a single next element. Also, it would be extremely useful if you implemented each instead of iterate, which then gives you the whole power of the Enumerable mixin.
class LinkedList
include Enumerable # THIS allows you to use `zip` :)
class Node # THIS because you didn't give us your Node
attr_accessor :next, :value
def initialize(value)
#value = value
#next = nil
end
end
def initialize(items)
#head = Node.new(items.shift)
items.inject(#head) { |last, data| #tail = last.next = Node.new(data) }
end
def each
return enum_for(__method__) unless block_given? # THIS allows block or blockless calls
return if #head.nil?
entry = #head
until entry.nil?
yield entry.value # THIS yields node values instead of nodes
entry = entry.next
end
end
def ==(other_list)
# and finally THIS - get pairs from self and other, and make sure all are equal
zip(other_list).all? { |a, b| a == b }
end
end
a = LinkedList.new([1, 2, 3])
b = LinkedList.new([1, 2, 3])
c = LinkedList.new([1, 2])
puts a == b # => true
puts a == c # => false
EDIT: I missed this on the first run through: equal? is supposed to be referential identity, i.e. two variables are equal? if they contain the reference to the same object. You should not redefine that method, even though it is possible. Rather, == is the general common-language meaning of "equal" as in "having the same value", so I changed it to that.
I think there is something wrong with your initialize method in LinkedList, regardless could this be what you need
...
def equal?(other_list)
other_index = 0
cur_index = 0
hash = Hash.new
other_list.iterate do |ol|
hash[ol.data.data] = other_index
other_index += 1
end
self.iterate do |node|
return false if hash[node.data.data] != cur_index
return false if !hash.has_key?(node.data.data)
cur_index += 1
end
return true
end
...
Assuming this is how you use your code
a = Node.new(1)
b = Node.new(2)
c = Node.new(3)
listA = [a,b,c]
aa = Node.new(1)
bb = Node.new(2)
cc = Node.new(3)
listB = [aa,bb,cc]
linkA = LinkedList.new(listA)
linkB = LinkedList.new(listB)
puts linkA.equal?(linkB)

Resources